Back to Blog
Engineering
Feb 15, 2024
12 min read

Building a High-Performance Job Scheduler in Go

Deep dive into how we built our job scheduling engine using Go for maximum performance and reliability.

Building a High-Performance Job Scheduler in Go

Performance and reliability are crucial when building a distributed job scheduler. Here's how we leveraged Go's strengths to create a high-performance scheduling engine that powers schedo.dev.

Why Go?

Our choice of Go was driven by several key requirements:

  1. Performance: Efficient garbage collection and fast compilation
  2. Concurrency: First-class support with goroutines and channels
  3. Simplicity: Clean syntax and strong standard library
  4. Production Ready: Battle-tested runtime and excellent tooling

Core Architecture

Our scheduler is built around several key components:

1. Job Queue Management

type JobQueue struct {

jobs map[JobID]*Job

mu sync.RWMutex

schedule *heap.PriorityQueue

}

func (q *JobQueue) Schedule(job *Job) error {

q.mu.Lock()

defer q.mu.Unlock()

// Implementation details

return nil

}

2. Time Management

Precise timing is crucial for a scheduler:

type TimeManager struct {

resolution time.Duration

lastTick atomic.Int64

}

func (tm *TimeManager) NextExecution(schedule *Schedule) time.Time {

// Implementation details

return time.Now()

}

3. Distributed Coordination

Ensuring single execution across distributed nodes:

type Coordinator struct {

nodeID string

consensus *raft.Raft

ctx context.Context

}

func (c *Coordinator) ClaimJob(ctx context.Context, jobID JobID) (bool, error) {

// Implementation details

return true, nil

}

Performance Optimizations

1. Efficient Concurrency

We use Go's concurrency primitives effectively:

type JobState struct {

status atomic.Int32

lastExecution atomic.Int64

executionCount atomic.Uint64

}

func (js *JobState) UpdateStatus(status int32) {

js.status.Store(status)

}

2. Memory Management

Our memory management strategy minimizes allocations:

type JobPool struct {

pool sync.Pool

}

func NewJobPool() *JobPool {

return &JobPool{

pool: sync.Pool{

New: func() interface{} {

return &Job{}

},

},

}

}

3. Channel-Based Communication

Using channels for efficient job coordination:

type Scheduler struct {

jobChan chan *Job

resultChan chan *Result

cancelChan chan JobID

workerPool *WorkerPool

}

func (s *Scheduler) processJobs(ctx context.Context) {

for {

select {

case job := <-s.jobChan:

s.workerPool.Submit(job)

case <-ctx.Done():

return

}

}

}

Results

Our Go implementation achieved impressive results:

  • Sub-millisecond scheduling latency

  • Memory usage under 50MB for 100k jobs

  • Zero downtime upgrades

  • Automatic failover in under 50ms

Conclusion

Building our scheduler in Go has proven to be an excellent choice. The combination of performance, simplicity, and robust concurrency primitives has allowed us to create a reliable system that our customers can depend on.

Want to learn more about our technical implementation? Check out our documentation or get started with schedo.dev today.