Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/js/compiler/pool_test.go
4538 views
1
package compiler
2
3
import (
4
"context"
5
"sync/atomic"
6
"testing"
7
"time"
8
9
"github.com/stretchr/testify/require"
10
11
syncutil "github.com/projectdiscovery/utils/sync"
12
)
13
14
// TestAddWithContextRespectsDeadline verifies that AddWithContext returns an
15
// error when the context deadline expires while waiting for a pool slot.
16
// Before the fix, Add() used context.Background() and would block indefinitely.
17
func TestAddWithContextRespectsDeadline(t *testing.T) {
18
pool, err := syncutil.New(syncutil.WithSize(1))
19
require.NoError(t, err)
20
21
// Fill the only slot.
22
pool.Add()
23
defer pool.Done()
24
25
// Try to acquire with a short deadline, should fail fast and not hang.
26
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
27
defer cancel()
28
29
start := time.Now()
30
err = pool.AddWithContext(ctx)
31
elapsed := time.Since(start)
32
33
require.Error(t, err, "AddWithContext should fail when pool is full and deadline expires")
34
require.Less(t, elapsed, 200*time.Millisecond, "AddWithContext should fail fast after deadline")
35
}
36
37
// TestWatchdogReleasesSlotOnDeadline verifies that the watchdog goroutine
38
// releases a pool slot when the execution deadline expires, even if the
39
// worker goroutine is still running (zombie). This is the core fix for
40
// pool slot starvation: without the watchdog, a zombie goroutine holds its
41
// slot via defer Done() until its network call eventually times out (or never).
42
func TestWatchdogReleasesSlotOnDeadline(t *testing.T) {
43
pool, err := syncutil.New(syncutil.WithSize(1))
44
require.NoError(t, err)
45
46
// Acquire the only slot (simulates a JS execution starting).
47
pool.Add()
48
49
// Set up the watchdog pattern (same as our fix in pool.go / non-pool.go).
50
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
51
defer cancel()
52
53
var slotReleased atomic.Bool
54
watchdogDone := make(chan struct{})
55
go func() {
56
select {
57
case <-ctx.Done():
58
if slotReleased.CompareAndSwap(false, true) {
59
pool.Done()
60
}
61
case <-watchdogDone:
62
}
63
}()
64
defer func() {
65
close(watchdogDone)
66
if slotReleased.CompareAndSwap(false, true) {
67
pool.Done()
68
}
69
}()
70
71
// Wait for the deadline to fire and the watchdog to release the slot.
72
<-ctx.Done()
73
time.Sleep(20 * time.Millisecond)
74
75
// A new execution should be able to acquire the slot, even though the
76
// "zombie" never called Done() itself.
77
freshCtx, freshCancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
78
defer freshCancel()
79
require.NoError(t, pool.AddWithContext(freshCtx),
80
"slot acquisition should succeed after watchdog release")
81
pool.Done()
82
}
83
84
// TestPoolExhaustionRecovery demonstrates the complete starvation/recovery
85
// cycle. All pool slots are filled with zombie goroutines that block well
86
// beyond their deadline. The watchdog pattern frees the slots when the
87
// deadlines expire, allowing subsequent executions to proceed.
88
//
89
// Without the fix, the pool stays permanently exhausted and every subsequent
90
// AddWithContext call fails (or Add() blocks forever).
91
func TestPoolExhaustionRecovery(t *testing.T) {
92
const poolSize = 3
93
pool, err := syncutil.New(syncutil.WithSize(poolSize))
94
require.NoError(t, err)
95
96
// Fill every slot with a "zombie" that blocks for 10s but has a 100ms
97
// deadline. The watchdog should free each slot after ~100ms.
98
for i := range poolSize {
99
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
100
defer cancel()
101
102
require.NoError(t, pool.AddWithContext(ctx), "initial slot acquisition %d", i)
103
104
var released atomic.Bool
105
done := make(chan struct{})
106
107
// Watchdog: release slot when deadline expires.
108
go func() {
109
select {
110
case <-ctx.Done():
111
if released.CompareAndSwap(false, true) {
112
pool.Done()
113
}
114
case <-done:
115
}
116
}()
117
118
// Zombie worker: blocks for 10s simulating a hung network call.
119
go func() {
120
defer func() {
121
close(done)
122
if released.CompareAndSwap(false, true) {
123
pool.Done()
124
}
125
}()
126
time.Sleep(10 * time.Second)
127
}()
128
}
129
130
// Pool is fully saturated. Wait for all deadlines to expire.
131
time.Sleep(200 * time.Millisecond)
132
133
// All slots should now be free. Acquire and release each one.
134
for i := range poolSize {
135
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
136
defer cancel()
137
require.NoError(t, pool.AddWithContext(ctx),
138
"post-recovery slot acquisition %d/%d (pool still starved)", i+1, poolSize)
139
pool.Done()
140
}
141
}
142
143