215 lines
4.9 KiB
Go
215 lines
4.9 KiB
Go
package plugin_ui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Job represents a task to be executed in the VM
|
|
type Job struct {
|
|
fn func() error
|
|
resultCh chan error
|
|
async bool // Flag to indicate if the job is async (doesn't need to wait for result)
|
|
}
|
|
|
|
// Scheduler handles all VM operations added concurrently in a single goroutine
|
|
// Any goroutine that needs to execute a VM operation must schedule it because the UI VM isn't thread safe
|
|
type Scheduler struct {
|
|
jobQueue chan *Job
|
|
ctx context.Context
|
|
context *Context
|
|
cancel context.CancelFunc
|
|
wg sync.WaitGroup
|
|
// Track the currently executing job to detect nested scheduling
|
|
currentJob *Job
|
|
currentJobLock sync.Mutex
|
|
}
|
|
|
|
func NewScheduler(uiCtx *Context) *Scheduler {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
s := &Scheduler{
|
|
jobQueue: make(chan *Job, 9999),
|
|
ctx: ctx,
|
|
context: uiCtx,
|
|
cancel: cancel,
|
|
}
|
|
|
|
s.start()
|
|
return s
|
|
}
|
|
|
|
func (s *Scheduler) start() {
|
|
s.wg.Add(1)
|
|
go func() {
|
|
defer s.wg.Done()
|
|
for {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
case job := <-s.jobQueue:
|
|
// Set the current job before execution
|
|
s.currentJobLock.Lock()
|
|
s.currentJob = job
|
|
s.currentJobLock.Unlock()
|
|
|
|
err := job.fn()
|
|
|
|
// Clear the current job after execution
|
|
s.currentJobLock.Lock()
|
|
s.currentJob = nil
|
|
s.currentJobLock.Unlock()
|
|
|
|
// Only send result if the job is not async
|
|
if !job.async {
|
|
job.resultCh <- err
|
|
}
|
|
|
|
if err != nil {
|
|
s.context.HandleException(err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *Scheduler) Stop() {
|
|
s.cancel()
|
|
s.wg.Wait()
|
|
}
|
|
|
|
// Schedule adds a job to the queue and waits for its completion
|
|
func (s *Scheduler) Schedule(fn func() error) error {
|
|
resultCh := make(chan error, 1)
|
|
job := &Job{
|
|
fn: func() error {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
resultCh <- fmt.Errorf("panic: %v", r)
|
|
}
|
|
}()
|
|
return fn()
|
|
},
|
|
resultCh: resultCh,
|
|
async: false,
|
|
}
|
|
|
|
// Check if we're already in a job execution context
|
|
s.currentJobLock.Lock()
|
|
isNestedCall := s.currentJob != nil && !s.currentJob.async
|
|
s.currentJobLock.Unlock()
|
|
|
|
// If this is a nested call from a synchronous job, we need to be careful
|
|
// We can't execute directly because the VM isn't thread-safe
|
|
// Instead, we'll queue it and use a separate goroutine to wait for the result
|
|
if isNestedCall {
|
|
// Queue the job
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return fmt.Errorf("scheduler stopped")
|
|
case s.jobQueue <- job:
|
|
// Create a separate goroutine to wait for the result
|
|
// This prevents deadlock while still ensuring the job runs in the scheduler
|
|
resultCh2 := make(chan error, 1)
|
|
go func() {
|
|
resultCh2 <- <-resultCh
|
|
}()
|
|
return <-resultCh2
|
|
}
|
|
}
|
|
|
|
// Otherwise, queue the job normally
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return fmt.Errorf("scheduler stopped")
|
|
case s.jobQueue <- job:
|
|
return <-resultCh
|
|
}
|
|
}
|
|
|
|
// ScheduleAsync adds a job to the queue without waiting for completion
|
|
// This is useful for fire-and-forget operations or when a job needs to schedule another job
|
|
func (s *Scheduler) ScheduleAsync(fn func() error) {
|
|
job := &Job{
|
|
fn: func() error {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
s.context.HandleException(fmt.Errorf("panic in async job: %v", r))
|
|
}
|
|
}()
|
|
return fn()
|
|
},
|
|
resultCh: nil, // No result channel needed
|
|
async: true,
|
|
}
|
|
|
|
// Queue the job without blocking
|
|
select {
|
|
case <-s.ctx.Done():
|
|
// Scheduler is stopped, just ignore
|
|
return
|
|
case s.jobQueue <- job:
|
|
// Job queued successfully
|
|
return
|
|
default:
|
|
// Queue is full, log an error
|
|
s.context.HandleException(fmt.Errorf("async job queue is full"))
|
|
}
|
|
}
|
|
|
|
// ScheduleWithTimeout schedules a job with a timeout
|
|
func (s *Scheduler) ScheduleWithTimeout(fn func() error, timeout time.Duration) error {
|
|
resultCh := make(chan error, 1)
|
|
job := &Job{
|
|
fn: func() error {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
resultCh <- fmt.Errorf("panic: %v", r)
|
|
}
|
|
}()
|
|
return fn()
|
|
},
|
|
resultCh: resultCh,
|
|
async: false,
|
|
}
|
|
|
|
// Check if we're already in a job execution context
|
|
s.currentJobLock.Lock()
|
|
isNestedCall := s.currentJob != nil && !s.currentJob.async
|
|
s.currentJobLock.Unlock()
|
|
|
|
// If this is a nested call from a synchronous job, handle it specially
|
|
if isNestedCall {
|
|
// Queue the job
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return fmt.Errorf("scheduler stopped")
|
|
case s.jobQueue <- job:
|
|
// Create a separate goroutine to wait for the result with timeout
|
|
resultCh2 := make(chan error, 1)
|
|
go func() {
|
|
select {
|
|
case err := <-resultCh:
|
|
resultCh2 <- err
|
|
case <-time.After(timeout):
|
|
resultCh2 <- fmt.Errorf("operation timed out")
|
|
}
|
|
}()
|
|
return <-resultCh2
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return fmt.Errorf("scheduler stopped")
|
|
case s.jobQueue <- job:
|
|
select {
|
|
case err := <-resultCh:
|
|
return err
|
|
case <-time.After(timeout):
|
|
return fmt.Errorf("operation timed out")
|
|
}
|
|
}
|
|
}
|