Skip to content

Commit 5ba0ae8

Browse files
committed
add a proc runner pool
1 parent 129c544 commit 5ba0ae8

3 files changed

Lines changed: 108 additions & 0 deletions

File tree

pkg/procrunner/pool.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package procrunner
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
)
7+
8+
var (
9+
ErrMaxReached = fmt.Errorf("max reached")
10+
)
11+
12+
// ProcRunnerPool is a manager that manages a pool of ProcRunner.
13+
// It can safely enforce the global memory limit by limiting the number of concurrent ProcRunners.
14+
type ProcRunnerPool struct {
15+
running int
16+
max int
17+
mu sync.Mutex
18+
}
19+
20+
func NewProcRunnerPool(maxConcurrent int) *ProcRunnerPool {
21+
return &ProcRunnerPool{
22+
running: 0,
23+
max: maxConcurrent,
24+
mu: sync.Mutex{},
25+
}
26+
}
27+
28+
func (p *ProcRunnerPool) Running() int {
29+
p.mu.Lock()
30+
defer p.mu.Unlock()
31+
return p.running
32+
}
33+
34+
func (p *ProcRunnerPool) Max() int {
35+
return p.max
36+
}
37+
38+
func (p *ProcRunnerPool) NewRunner(filename string, maxheapsizemb uint) (*ProcRunner, error) {
39+
p.mu.Lock()
40+
defer p.mu.Unlock()
41+
if p.running >= p.max {
42+
return nil, ErrMaxReached
43+
}
44+
runner, err := NewProcRunner(filename, maxheapsizemb)
45+
if err != nil {
46+
return nil, err
47+
}
48+
runner.AddPostCloseFn(func() {
49+
p.mu.Lock()
50+
defer p.mu.Unlock()
51+
p.running--
52+
})
53+
p.running++
54+
return runner, nil
55+
}

pkg/procrunner/pool_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package procrunner
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/suite"
7+
)
8+
9+
type ProcRunnerPoolTestSuite struct {
10+
suite.Suite
11+
}
12+
13+
func TestProcRunnerPoolTestSuite(t *testing.T) {
14+
suite.Run(t, new(ProcRunnerPoolTestSuite))
15+
}
16+
17+
func (suite *ProcRunnerPoolTestSuite) SetupTest() {
18+
}
19+
20+
func (suite *ProcRunnerPoolTestSuite) TestConcurrency() {
21+
pool := NewProcRunnerPool(2)
22+
suite.Equal(0, pool.Running())
23+
suite.Equal(2, pool.Max())
24+
runner1, err := pool.NewRunner("test.js", 32)
25+
suite.NoError(err)
26+
suite.Equal(1, pool.Running())
27+
suite.Equal(2, pool.Max())
28+
runner2, err := pool.NewRunner("test.js", 32)
29+
suite.NoError(err)
30+
suite.Equal(2, pool.Running())
31+
runner3, err := pool.NewRunner("test.js", 32)
32+
suite.Equal(err, ErrMaxReached)
33+
suite.Nil(runner3)
34+
runner1.Close()
35+
suite.Equal(1, pool.Running())
36+
runner4, err := pool.NewRunner("test.js", 32)
37+
suite.NoError(err)
38+
runner2.Close()
39+
runner4.Close()
40+
suite.Equal(0, pool.Running())
41+
}

pkg/procrunner/procrunner.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ type ProcRunner struct {
3939
wg sync.WaitGroup
4040
closeFn func()
4141
closed atomic.Bool
42+
43+
postCloseFn []func()
4244
}
4345

4446
// NewProcRunner creates a new ProcRunner that runs the given file.
@@ -87,6 +89,10 @@ func NewProcRunner(fileName string, maxHeapSizeMB uint) (*ProcRunner, error) {
8789
defer proc.wg.Done()
8890
_ = cmd.Wait()
8991
proc.closed.Store(true)
92+
// call postCloseFn only after the process is killed
93+
for _, f := range proc.postCloseFn {
94+
f()
95+
}
9096
}()
9197
// // handle stderr
9298
// proc.wg.Add(1)
@@ -191,3 +197,9 @@ func (r *ProcRunner) RunCodeJSON(ctx context.Context, code string) (string, erro
191197
return "", ErrorTimeout
192198
}
193199
}
200+
201+
// AddPostCloseFn adds a function to be called after the runner is closed.
202+
// NOTE: NOT concurrency safe.
203+
func (r *ProcRunner) AddPostCloseFn(f func()) {
204+
r.postCloseFn = append(r.postCloseFn, f)
205+
}

0 commit comments

Comments
 (0)