Skip to content
Snippets Groups Projects
Verified Commit 55adbfcd authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: implement jobqueue

parent 5f8be4ff
No related branches found
No related tags found
No related merge requests found
{"version":"0.1.0"}
package jobqueue
import (
"bytes"
"context"
"fmt"
"os/exec"
)
type Runnable interface {
Run(ctx context.Context) (int, any, error)
}
type GoFunctionRunner struct {
Func func() (int, any, error)
}
type Result struct {
Code int
Result any
Err error
}
func (g *GoFunctionRunner) Run(ctx context.Context) (int, any, error) {
done := make(chan Result)
go func() {
var res Result
defer func() {
if r := recover(); r != nil {
res.Err = fmt.Errorf("Command panicked: %w", fmt.Errorf("%v", r))
}
done <- res
}()
res.Code, res.Result, res.Err = g.Func()
}()
select {
case res := <-done:
return res.Code, res.Result, res.Err
case <-ctx.Done():
return RunnableTerminatedExitCode, nil, ctx.Err()
}
}
type ExternalProcessRunner struct {
Command string
Args []string
}
func (e *ExternalProcessRunner) Run(ctx context.Context) (int, any, error) {
var stdout, stderr bytes.Buffer
cmd := exec.CommandContext(ctx, e.Command, e.Args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
done := make(chan Result)
go func() {
var res Result
defer func() {
if r := recover(); r != nil {
res.Err = fmt.Errorf("Command panicked: %w", fmt.Errorf("%v", r))
}
done <- res
}()
err := cmd.Run()
res.Result = stdout.String() + stderr.String()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
res.Code = exitErr.ExitCode()
}
res.Err = err
} else {
res.Code = cmd.ProcessState.ExitCode()
}
}()
select {
case res := <-done:
return res.Code, res.Result, res.Err
case <-ctx.Done():
if cmd.Process != nil {
_ = cmd.Process.Kill()
}
return RunnableTerminatedExitCode, nil, ctx.Err()
}
}
package jobqueue
import (
"context"
"testing"
"time"
)
func TestGoFunctionRunner(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
runner := &GoFunctionRunner{
Func: func() (int, any, error) {
return 42, nil, nil
},
}
result, _, err := runner.Run(ctx)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if result != 42 {
t.Errorf("Expected 42, got %v", result)
}
}
func TestGoFunctionRunnerTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
defer cancel()
runner := &GoFunctionRunner{
Func: func() (int, any, error) {
time.Sleep(10 * time.Millisecond)
return 42, nil, nil
},
}
_, _, err := runner.Run(ctx)
if err == nil {
t.Errorf("Expected timeout error, got nil")
}
}
func TestExternalProcessRunner(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Hour)
defer cancel()
runner := &ExternalProcessRunner{
Command: "echo",
Args: []string{"hello"},
}
_, _, err := runner.Run(ctx)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func TestExternalProcessRunnerFail(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
runner := &ExternalProcessRunner{
Command: "nonexistentcommand",
}
_, _, err := runner.Run(ctx)
if err == nil {
t.Errorf("Expected error, got nil")
}
}
func TestGoFunctionRunnerNilFunc(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
runner := &GoFunctionRunner{}
_, _, err := runner.Run(ctx)
if err == nil {
t.Errorf("Expected error, got nil")
}
}
func TestGoFunctionRunnerPanic(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
runner := &GoFunctionRunner{
Func: func() (int, any, error) {
panic("Test panic")
},
}
_, _, err := runner.Run(ctx)
if err == nil {
t.Errorf("Expected error, got nil")
}
}
func TestGoFunctionRunnerExpiredContext(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
defer cancel()
time.Sleep(2 * time.Nanosecond)
runner := &GoFunctionRunner{
Func: func() (int, any, error) {
return 42, nil, nil
},
}
_, _, err := runner.Run(ctx)
if err == nil {
t.Errorf("Expected context expired error, got nil")
}
}
func TestExternalProcessRunnerInvalidCommand(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
runner := &ExternalProcessRunner{
Command: "",
}
_, _, err := runner.Run(ctx)
if err == nil {
t.Errorf("Expected error for invalid command, got nil")
}
}
stat.go 0 → 100644
package jobqueue
import (
"context"
"github.com/shirou/gopsutil/v3/cpu"
"math"
"runtime"
"sync"
"sync/atomic"
"time"
)
var mainResourceStats *ResourceStats
func StartResourceMonitoring(interval time.Duration) error {
mainResourceStats = NewResourceStats()
return mainResourceStats.StartMonitoring(interval)
}
func StopResourceMonitoring() {
if mainResourceStats != nil {
mainResourceStats.StopMonitoring()
}
}
func resetResourceStatsForTesting() {
if mainResourceStats != nil {
StopResourceMonitoring()
}
}
func GetCpuUsage() float64 {
if mainResourceStats != nil {
return mainResourceStats.GetCpuUsage()
}
return 0
}
func GetMemoryUsage() uint64 {
if mainResourceStats != nil {
return mainResourceStats.GetMemoryUsage()
}
return 0
}
type ResourceStats struct {
cpuUsage uint64
memoryUsage uint64
context context.Context
cancel context.CancelFunc
mu sync.Mutex
}
func NewResourceStats() *ResourceStats {
return &ResourceStats{}
}
func (stats *ResourceStats) getMemoryUsage() uint64 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.Alloc
}
func (stats *ResourceStats) getCPUPercentage() (float64, error) {
percentages, err := cpu.Percent(100*time.Millisecond, false)
if err != nil {
return 0, err
}
if len(percentages) == 0 {
return 0, ErrCPUPercentage
}
return percentages[0], nil
}
func (stats *ResourceStats) assignResourceStats() {
mem := stats.getMemoryUsage()
cpuP, err := stats.getCPUPercentage()
if err != nil {
return
}
cpuPBits := math.Float64bits(cpuP)
atomic.StoreUint64(&stats.cpuUsage, cpuPBits)
atomic.StoreUint64(&stats.memoryUsage, mem)
}
func (stats *ResourceStats) MonitorResources(interval time.Duration) {
stats.mu.Lock()
ctx := stats.context
stats.mu.Unlock()
if ctx == nil {
return
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
stats.assignResourceStats()
case <-ctx.Done():
return
}
}
}
func (stats *ResourceStats) StartMonitoring(interval time.Duration) error {
stats.mu.Lock()
defer stats.mu.Unlock()
if stats.context != nil && stats.context.Err() == nil {
return nil
}
ctx, cancel := context.WithCancel(context.Background())
stats.context = ctx
stats.cancel = cancel
if interval == 0 {
return ErrIntervalIsZero
}
stats.assignResourceStats()
go stats.MonitorResources(interval)
return nil
}
func (stats *ResourceStats) StopMonitoring() {
stats.mu.Lock()
defer stats.mu.Unlock()
if stats.cancel != nil {
ctx := stats.context // save for later
stats.context = nil // set to nil first
stats.cancel() // then cancel
ctx.Done() // ensure channel is closed if needed
stats.cancel = nil
}
}
func (stats *ResourceStats) GetCpuUsage() float64 {
bits := atomic.LoadUint64(&stats.cpuUsage)
return math.Float64frombits(bits)
}
func (stats *ResourceStats) GetMemoryUsage() uint64 {
return atomic.LoadUint64(&stats.memoryUsage)
}
package jobqueue
import (
"github.com/stretchr/testify/assert"
"math"
"sync/atomic"
"testing"
"time"
)
func TestResourceStats(t *testing.T) {
stats := NewResourceStats()
expectedCPUUsage := 75.5
expectedMemoryUsage := uint64(1024 * 1024) // 1 MB
cpuBits := math.Float64bits(expectedCPUUsage)
atomic.StoreUint64(&stats.cpuUsage, cpuBits)
atomic.StoreUint64(&stats.memoryUsage, expectedMemoryUsage)
actualCPUUsage := stats.GetCpuUsage()
actualMemoryUsage := stats.GetMemoryUsage()
assert.InDelta(t, expectedCPUUsage, actualCPUUsage, 0.001) // Überprüfe auf Genauigkeit bis auf 0,001
assert.Equal(t, expectedMemoryUsage, actualMemoryUsage)
}
func TestResourceMonitoringStartStop(t *testing.T) {
stats := NewResourceStats()
err := stats.StartMonitoring(1 * time.Second)
assert.Nil(t, err)
defer stats.StopMonitoring()
time.Sleep(100 * time.Millisecond)
assert.NotNil(t, stats.context)
assert.Nil(t, stats.context.Err())
stats.StopMonitoring()
time.Sleep(100 * time.Millisecond)
assert.Nil(t, stats.context)
}
package jobqueue
import "container/heap"
// JobIDPriority is a type that holds a JobID and its Priority
type JobIDPriority struct {
ID JobIDType
Priority int
}
// JobIDPriorityQueue is a priority jobs for JobIDPriority
type JobIDPriorityQueue []JobIDPriority
// Len implements heap.Interface.Len
func (pq JobIDPriorityQueue) Len() int { return len(pq) }
// Less implements heap.Interface.Less
func (pq JobIDPriorityQueue) Less(i, j int) bool {
return pq[i].Priority > pq[j].Priority
}
// Swap implements heap.Interface.Swap
func (pq JobIDPriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
}
// Push implements heap.Interface.Push
func (pq *JobIDPriorityQueue) Push(x interface{}) {
item := x.(JobIDPriority)
*pq = append(*pq, item)
}
// Pop implements heap.Interface.Pop
func (pq *JobIDPriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
*pq = old[0 : n-1]
return item
}
// topologicalSortJobs returns a topologically sorted list of job IDs
func topologicalSortJobs(jobs map[JobIDType]*job) ([]JobIDType, error) {
// Initialize in-degrees
inDegrees := make(map[JobIDType]int)
for id := range jobs {
inDegrees[id] = 0
}
for _, job := range jobs {
for _, dependency := range job.Dependencies {
// check if dependency exists
if _, ok := jobs[dependency]; !ok {
return nil, ErrMissingDependency
}
inDegrees[dependency]++
}
}
// Create a priority jobs
pq := make(JobIDPriorityQueue, 0)
heap.Init(&pq)
// Add jobs with zero in-degree to priority jobs
for id, inDegree := range inDegrees {
if inDegree == 0 {
heap.Push(&pq, JobIDPriority{ID: id, Priority: jobs[id].Priority})
}
}
result := make([]JobIDType, 0)
for len(pq) > 0 {
jobIDPriority := heap.Pop(&pq).(JobIDPriority)
jobID := jobIDPriority.ID
result = append(result, jobID)
for _, dependentJobID := range jobs[jobID].Dependencies {
inDegrees[dependentJobID]--
if inDegrees[dependentJobID] == 0 {
heap.Push(&pq, JobIDPriority{ID: dependentJobID, Priority: jobs[dependentJobID].Priority})
}
}
}
// Check for cycles
for _, inDegree := range inDegrees {
if inDegree > 0 {
return nil, ErrCycleDetected
}
}
return result, nil
}
package jobqueue
import (
"reflect"
"testing"
)
func TestTopologicalSortJobs(t *testing.T) {
// Create a sample set of jobs with dependencies and priorities
job1 := &job{JobSpecification: JobSpecification{Id: "1", Priority: PriorityHigh}}
job2 := &job{JobSpecification: JobSpecification{Id: "2", Priority: PriorityHigh, Dependencies: []JobIDType{"1"}}}
job3 := &job{JobSpecification: JobSpecification{Id: "3", Priority: PriorityLow, Dependencies: []JobIDType{"1"}}}
job4 := &job{JobSpecification: JobSpecification{Id: "4", Priority: PriorityCritical, Dependencies: []JobIDType{"3"}}}
job5 := &job{JobSpecification: JobSpecification{Id: "5", Dependencies: []JobIDType{"2", "4"}}}
jobs := map[JobIDType]*job{
"1": job1,
"2": job2,
"3": job3,
"4": job4,
"5": job5,
}
// Call the function to get the sorted job IDs
sortedJobIDs, err := topologicalSortJobs(jobs)
if err != nil {
t.Errorf("Error in sorting jobs: %v", err)
}
// Define the expected order
expectedOrder := []JobIDType{"5", "4", "2", "3", "1"}
// Check if the result matches the expected order
if !reflect.DeepEqual(sortedJobIDs, expectedOrder) {
t.Errorf("Expected order %v, but got %v", expectedOrder, sortedJobIDs)
}
}
func TestTopologicalSortJobs2(t *testing.T) {
// Create a sample set of jobs with dependencies and priorities
job1 := &job{JobSpecification: JobSpecification{Id: "1", Priority: PriorityHigh}}
job2 := &job{JobSpecification: JobSpecification{Id: "2", Priority: PriorityHigh, Dependencies: []JobIDType{"1"}}}
job3 := &job{JobSpecification: JobSpecification{Id: "3", Priority: PriorityLow, Dependencies: []JobIDType{"1"}}}
job4 := &job{JobSpecification: JobSpecification{Id: "4", Priority: PriorityCritical, Dependencies: []JobIDType{"3"}}}
job5 := &job{JobSpecification: JobSpecification{Id: "5", Dependencies: []JobIDType{"2", "4"}}}
jobs := map[JobIDType]*job{
"1": job1,
"2": job2,
"3": job3,
"4": job4,
"5": job5,
}
// Call the function to get the sorted job IDs
sortedJobIDs, err := topologicalSortJobs(jobs)
if err != nil {
t.Errorf("Error in sorting jobs: %v", err)
}
// Define the expected order
expectedOrder := []JobIDType{"5", "4", "2", "3", "1"}
// Check if the result matches the expected order
if !reflect.DeepEqual(sortedJobIDs, expectedOrder) {
t.Errorf("Expected order %v, but got %v", expectedOrder, sortedJobIDs)
}
}
func TestTopologicalSortJobsNoDependencies(t *testing.T) {
// Create a sample set of jobs with no dependencies
job1 := &job{JobSpecification: JobSpecification{Id: "1"}}
job2 := &job{JobSpecification: JobSpecification{Id: "2"}}
job3 := &job{JobSpecification: JobSpecification{Id: "3"}}
jobs := map[JobIDType]*job{
"1": job1,
"2": job2,
"3": job3,
}
// Call the function to get the sorted job IDs
sortedJobIDs, err := topologicalSortJobs(jobs)
if err != nil {
t.Errorf("Error in sorting jobs: %v", err)
}
// Define the expected order (in any order because they have no dependencies)
expectedOrder := []JobIDType{"1", "2", "3"}
// Check if the result contains the same elements as the expected order
if len(sortedJobIDs) != len(expectedOrder) {
t.Errorf("Expected order %v, but got %v", expectedOrder, sortedJobIDs)
}
}
func TestTopologicalSortJobs_EmptyMap(t *testing.T) {
jobs := map[JobIDType]*job{}
sortedJobIDs, err := topologicalSortJobs(jobs)
if err != nil {
t.Errorf("Error in sorting jobs: %v", err)
}
if len(sortedJobIDs) != 0 {
t.Errorf("Expected empty slice, got %v", sortedJobIDs)
}
}
func TestTopologicalSortJobs_CycleDetected(t *testing.T) {
// Creating a cycle 1 -> 2 -> 3 -> 1
job1 := &job{JobSpecification: JobSpecification{Id: "1", Dependencies: []JobIDType{"3"}}}
job2 := &job{JobSpecification: JobSpecification{Id: "2", Dependencies: []JobIDType{"1"}}}
job3 := &job{JobSpecification: JobSpecification{Id: "3", Dependencies: []JobIDType{"2"}}}
jobs := map[JobIDType]*job{
"1": job1,
"2": job2,
"3": job3,
}
_, err := topologicalSortJobs(jobs)
if err != ErrCycleDetected {
t.Errorf("Expected ErrCycleDetected, got %v", err)
}
}
func TestTopologicalSortJobs_SingleNode(t *testing.T) {
job1 := &job{JobSpecification: JobSpecification{Id: "1"}}
jobs := map[JobIDType]*job{
"1": job1,
}
sortedJobIDs, err := topologicalSortJobs(jobs)
if err != nil {
t.Errorf("Error in sorting jobs: %v", err)
}
if !reflect.DeepEqual(sortedJobIDs, []JobIDType{"1"}) {
t.Errorf("Expected [\"1\"], got %v", sortedJobIDs)
}
}
func TestTopologicalSortJobs_MissingDependency(t *testing.T) {
job1 := &job{JobSpecification: JobSpecification{Id: "1"}}
job2 := &job{JobSpecification: JobSpecification{Id: "2", Dependencies: []JobIDType{"3"}}}
jobs := map[JobIDType]*job{
"1": job1,
"2": job2,
}
_, err := topologicalSortJobs(jobs)
if err != nil && err != ErrMissingDependency {
t.Errorf("Expected ErrMissingDependency, got %v", err)
}
}
func TestTopologicalSortJobs_SelfDependency(t *testing.T) {
// job 1 depends on itself
job1 := &job{JobSpecification: JobSpecification{Id: "1", Dependencies: []JobIDType{"1"}}}
jobs := map[JobIDType]*job{
"1": job1,
}
_, err := topologicalSortJobs(jobs)
if err != ErrCycleDetected {
t.Errorf("Expected ErrCycleDetected, got %v", err)
}
}
func TestTopologicalSortJobs_MultipleEdges(t *testing.T) {
// job 3 and job 4 both depend on job 2
job1 := &job{JobSpecification: JobSpecification{Id: "1"}}
job2 := &job{JobSpecification: JobSpecification{Id: "2", Dependencies: []JobIDType{"1"}}}
job3 := &job{JobSpecification: JobSpecification{Id: "3", Dependencies: []JobIDType{"2"}}}
job4 := &job{JobSpecification: JobSpecification{Id: "4", Dependencies: []JobIDType{"2"}}}
jobs := map[JobIDType]*job{
"1": job1,
"2": job2,
"3": job3,
"4": job4,
}
sortedJobIDs, err := topologicalSortJobs(jobs)
if err != nil {
t.Errorf("Error in sorting jobs: %v", err)
}
if !reflect.DeepEqual(sortedJobIDs, []JobIDType{"4", "3", "2", "1"}) && !reflect.DeepEqual(sortedJobIDs, []JobIDType{"3", "4", "2", "1"}) {
t.Errorf("Unexpected order: %v", sortedJobIDs)
}
}
func TestTopologicalSortJobs_MultipleDependencies(t *testing.T) {
// job 3 depends on both job 1 and job 2
job1 := &job{JobSpecification: JobSpecification{Id: "1"}}
job2 := &job{JobSpecification: JobSpecification{Id: "2"}}
job3 := &job{JobSpecification: JobSpecification{Id: "3", Dependencies: []JobIDType{"1", "2"}}}
jobs := map[JobIDType]*job{
"1": job1,
"2": job2,
"3": job3,
}
sortedJobIDs, err := topologicalSortJobs(jobs)
if err != nil {
t.Errorf("Error in sorting jobs: %v", err)
}
if !reflect.DeepEqual(sortedJobIDs, []JobIDType{"3", "2", "1"}) && !reflect.DeepEqual(sortedJobIDs, []JobIDType{"3", "1", "2"}) {
t.Errorf("Unexpected order: %v", sortedJobIDs)
}
}
func TestTopologicalSortJobs_PriorityIgnoredInCycle(t *testing.T) {
// Cycle exists even if one job has high priority
job1 := &job{JobSpecification: JobSpecification{Id: "1", Priority: PriorityHigh, Dependencies: []JobIDType{"2"}}}
job2 := &job{JobSpecification: JobSpecification{Id: "2", Dependencies: []JobIDType{"1"}}}
jobs := map[JobIDType]*job{
"1": job1,
"2": job2,
}
_, err := topologicalSortJobs(jobs)
if err != ErrCycleDetected {
t.Errorf("Expected ErrCycleDetected, got %v", err)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment