Skip to content

Commit 261e0d3

Browse files
committed
add parallel checks
Signed-off-by: Mohammed Naser <mnaser@vexxhost.com>
1 parent baf44f7 commit 261e0d3

10 files changed

Lines changed: 564 additions & 105 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/mattn/go-isatty v0.0.17 // indirect
2424
github.com/pmezard/go-difflib v1.0.0 // indirect
2525
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
26+
golang.org/x/sync v0.17.0 // indirect
2627
golang.org/x/sys v0.33.0 // indirect
2728
golang.org/x/text v0.20.0 // indirect
2829
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
3535
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
3636
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
3737
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
38+
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
39+
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
3840
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3941
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
4042
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

main.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import (
1414
"time"
1515

1616
"github.com/vexxhost/ovsinit/pkg/appctl"
17-
"github.com/vexxhost/ovsinit/pkg/succession" // Uses the history.go version
17+
"github.com/vexxhost/ovsinit/pkg/succession"
18+
"github.com/vexxhost/ovsinit/pkg/verifier"
1819
)
1920

2021
var (
@@ -186,6 +187,24 @@ func main() {
186187
os.Exit(1)
187188
}
188189

190+
verifiers := []verifier.Verifier{
191+
verifier.FileRemoval(fmt.Sprintf("%s/%s.pid", appctl.RUN_DIR, binary)),
192+
verifier.FileRemoval(fmt.Sprintf("%s/%s.*.ctl", appctl.RUN_DIR, binary)),
193+
}
194+
195+
if binary == "ovs-vswitchd" {
196+
verifiers = append(verifiers, verifier.HugePages())
197+
}
198+
199+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
200+
defer cancel()
201+
202+
err = verifier.Run(ctx, verifiers...)
203+
if err != nil {
204+
slog.Error("verification after exit failed", "error", err)
205+
os.Exit(1)
206+
}
207+
189208
slog.Info("stopped existing process")
190209
}
191210

pkg/appctl/appctl.go

Lines changed: 1 addition & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,12 @@ import (
55
"errors"
66
"fmt"
77
"io"
8-
"log/slog"
98
"net"
109
"os"
1110
"path/filepath"
12-
"time"
1311

1412
"github.com/cenkalti/rpc2"
1513
"github.com/cenkalti/rpc2/jsonrpc"
16-
"github.com/fsnotify/fsnotify"
17-
"github.com/prometheus/procfs"
1814
)
1915

2016
const (
@@ -92,104 +88,5 @@ func Cleanup(binary string) error {
9288
}
9389

9490
func (c *Client) Exit(ctx context.Context, binary string) error {
95-
pidPath := fmt.Sprintf("%s/%s.pid", RUN_DIR, binary)
96-
ctlPattern := fmt.Sprintf("%s/%s.*.ctl", RUN_DIR, binary)
97-
98-
watcher, err := fsnotify.NewWatcher()
99-
if err != nil {
100-
return fmt.Errorf("failed to create file watcher: %w", err)
101-
}
102-
defer watcher.Close()
103-
104-
err = watcher.Add(RUN_DIR)
105-
if err != nil {
106-
return fmt.Errorf("failed to watch directory %s: %w", RUN_DIR, err)
107-
}
108-
109-
err = c.CallWithContext(ctx, "exit", []string{}, nil)
110-
if err != nil {
111-
return fmt.Errorf("failed to send exit command: %w", err)
112-
}
113-
114-
timeout := time.After(30 * time.Second)
115-
116-
pidRemoved := false
117-
ctlRemoved := false
118-
119-
for {
120-
select {
121-
case event, ok := <-watcher.Events:
122-
if !ok {
123-
return errors.New("watcher channel closed")
124-
}
125-
if event.Op&fsnotify.Remove == fsnotify.Remove {
126-
if event.Name == pidPath {
127-
pidRemoved = true
128-
slog.Info("pid file removed", "pid_file", pidPath)
129-
} else if matched, _ := filepath.Match(ctlPattern, event.Name); matched {
130-
ctlRemoved = true
131-
slog.Info("ctl file removed", "ctl_file", event.Name)
132-
}
133-
134-
if pidRemoved && ctlRemoved {
135-
slog.Info("files removed, waiting for hugepages to be available")
136-
137-
// Get initial hugepages state
138-
fs, err := procfs.NewDefaultFS()
139-
if err != nil {
140-
slog.Warn("cannot access procfs, skipping hugepages check", "error", err)
141-
return nil
142-
}
143-
144-
// Poll until hugepages are available
145-
ticker := time.NewTicker(50 * time.Millisecond)
146-
defer ticker.Stop()
147-
hugepagesTimeout := time.After(5 * time.Second)
148-
149-
for {
150-
select {
151-
case <-ticker.C:
152-
memInfo, err := fs.Meminfo()
153-
if err != nil {
154-
slog.Warn("cannot read meminfo, assuming process exited", "error", err)
155-
return nil
156-
}
157-
158-
// Check if there are free hugepages
159-
if memInfo.HugePagesFree != nil && *memInfo.HugePagesFree > 0 {
160-
slog.Info("hugepages available, process fully exited",
161-
"hugepages_free", *memInfo.HugePagesFree)
162-
return nil
163-
}
164-
165-
// Also check if no hugepages are configured
166-
if memInfo.HugePagesTotal != nil && *memInfo.HugePagesTotal == 0 {
167-
slog.Info("no hugepages configured, process fully exited")
168-
return nil
169-
}
170-
171-
slog.Debug("waiting for hugepages to be freed",
172-
"hugepages_total", memInfo.HugePagesTotal,
173-
"hugepages_free", memInfo.HugePagesFree)
174-
175-
case <-hugepagesTimeout:
176-
slog.Warn("timeout waiting for hugepages, proceeding anyway")
177-
return nil
178-
case <-ctx.Done():
179-
return ctx.Err()
180-
}
181-
}
182-
}
183-
}
184-
case err, ok := <-watcher.Errors:
185-
if !ok {
186-
return errors.New("watcher error channel closed")
187-
}
188-
return fmt.Errorf("watcher error: %w", err)
189-
case <-timeout:
190-
return fmt.Errorf("timeout waiting for process to exit (pid_removed=%v, ctl_removed=%v)", pidRemoved, ctlRemoved)
191-
case <-ctx.Done():
192-
return ctx.Err()
193-
}
194-
}
91+
return c.CallWithContext(ctx, "exit", []string{}, nil)
19592
}

pkg/verifier/file_removal.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package verifier
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log/slog"
8+
"path/filepath"
9+
10+
"github.com/fsnotify/fsnotify"
11+
)
12+
13+
type FileRemovalVerifier struct {
14+
pattern string
15+
}
16+
17+
func FileRemoval(pattern string) *FileRemovalVerifier {
18+
return &FileRemovalVerifier{
19+
pattern: pattern,
20+
}
21+
}
22+
23+
func (v *FileRemovalVerifier) String() string {
24+
return fmt.Sprintf("file_removal(%s)", v.pattern)
25+
}
26+
27+
func (v *FileRemovalVerifier) Verify(ctx context.Context) error {
28+
// Check if files matching the pattern exist
29+
matches, err := filepath.Glob(v.pattern)
30+
if err != nil {
31+
return fmt.Errorf("failed to check pattern %s: %w", v.pattern, err)
32+
}
33+
34+
if len(matches) == 0 {
35+
slog.Info(fmt.Sprintf("%s: no files found, already removed", v.String()))
36+
return nil
37+
}
38+
39+
watcher, err := fsnotify.NewWatcher()
40+
if err != nil {
41+
return fmt.Errorf("failed to create watcher: %w", err)
42+
}
43+
defer func() {
44+
if err := watcher.Close(); err != nil {
45+
slog.Warn("failed to close watcher", "error", err)
46+
}
47+
}()
48+
49+
dir := filepath.Dir(v.pattern)
50+
if err := watcher.Add(dir); err != nil {
51+
return fmt.Errorf("failed to watch directory %s: %w", dir, err)
52+
}
53+
54+
for {
55+
select {
56+
case event, ok := <-watcher.Events:
57+
if !ok {
58+
return errors.New("watcher channel closed")
59+
}
60+
61+
if event.Op&fsnotify.Remove == fsnotify.Remove {
62+
matches, _ := filepath.Match(v.pattern, event.Name)
63+
64+
if matches {
65+
slog.Info(fmt.Sprintf("%s: file removed", v.String()),
66+
"file", event.Name)
67+
return nil
68+
}
69+
}
70+
case err, ok := <-watcher.Errors:
71+
if !ok {
72+
return errors.New("watcher error channel closed")
73+
}
74+
75+
return fmt.Errorf("watcher error: %w", err)
76+
case <-ctx.Done():
77+
return fmt.Errorf("timeout waiting for %s: %w", v.String(), ctx.Err())
78+
}
79+
}
80+
}

pkg/verifier/file_removal_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package verifier
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestFileRemovalVerifier(t *testing.T) {
15+
tempDir := t.TempDir()
16+
17+
// Test exact path matching
18+
testFile := filepath.Join(tempDir, "test.txt")
19+
err := os.WriteFile(testFile, []byte("test"), 0644)
20+
require.NoError(t, err)
21+
22+
verifier := FileRemoval(testFile)
23+
go func() {
24+
time.Sleep(100 * time.Millisecond)
25+
err = os.Remove(testFile)
26+
require.NoError(t, err)
27+
}()
28+
29+
err = verifier.Verify(t.Context())
30+
assert.NoError(t, err)
31+
}
32+
33+
func TestFileRemovalVerifierWithGlob(t *testing.T) {
34+
tempDir := t.TempDir()
35+
36+
testFile := filepath.Join(tempDir, "test.12345.log")
37+
err := os.WriteFile(testFile, []byte("test"), 0644)
38+
require.NoError(t, err)
39+
40+
pattern := filepath.Join(tempDir, "test.*.log")
41+
verifier := FileRemoval(pattern)
42+
go func() {
43+
time.Sleep(100 * time.Millisecond)
44+
err := os.Remove(testFile)
45+
require.NoError(t, err)
46+
}()
47+
48+
err = verifier.Verify(t.Context())
49+
assert.NoError(t, err)
50+
}
51+
52+
func TestFileRemovalVerifier_AlreadyRemoved(t *testing.T) {
53+
tempDir := t.TempDir()
54+
55+
pattern := filepath.Join(tempDir, "nonexistent.txt")
56+
verifier := FileRemoval(pattern)
57+
58+
err := verifier.Verify(t.Context())
59+
assert.NoError(t, err)
60+
}
61+
62+
func TestFileRemovalVerifier_AlreadyRemovedGlob(t *testing.T) {
63+
tempDir := t.TempDir()
64+
65+
pattern := filepath.Join(tempDir, "*.nonexistent")
66+
verifier := FileRemoval(pattern)
67+
68+
err := verifier.Verify(t.Context())
69+
assert.NoError(t, err)
70+
}
71+
72+
func TestFileRemovalVerifier_Timeout(t *testing.T) {
73+
tempDir := t.TempDir()
74+
75+
testFile := filepath.Join(tempDir, "persistent.txt")
76+
err := os.WriteFile(testFile, []byte("test"), 0644)
77+
require.NoError(t, err)
78+
79+
verifier := FileRemoval(testFile)
80+
81+
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond)
82+
defer cancel()
83+
84+
err = verifier.Verify(ctx)
85+
assert.Error(t, err)
86+
assert.Contains(t, err.Error(), "timeout")
87+
}
88+
89+
func TestFileRemovalVerifier_ContextCancellation(t *testing.T) {
90+
tempDir := t.TempDir()
91+
92+
testFile := filepath.Join(tempDir, "persistent.txt")
93+
err := os.WriteFile(testFile, []byte("test"), 0644)
94+
require.NoError(t, err)
95+
96+
verifier := FileRemoval(testFile)
97+
98+
ctx, cancel := context.WithCancel(t.Context())
99+
go func() {
100+
time.Sleep(10 * time.Millisecond)
101+
cancel()
102+
}()
103+
104+
err = verifier.Verify(ctx)
105+
assert.Error(t, err)
106+
assert.ErrorIs(t, err, context.Canceled)
107+
}

0 commit comments

Comments
 (0)