Skip to content

Commit 64011fa

Browse files
authored
fix: make file update atomic, wait for /start (#329)
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
1 parent e1bfc39 commit 64011fa

4 files changed

Lines changed: 55 additions & 50 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.idea
22
node_modules
33
bin/
4+
# dynamically generated flags
5+
flags/allFlags.json

launchpad/pkg/file.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ func ToggleChangingFlag() (string, error) {
8686

8787
// Write the updated JSON back to the file
8888
fileLock.Lock()
89-
if err := os.WriteFile(configFile, updatedData, 0644); err != nil {
89+
if err := atomicWriteFile(configFile, updatedData); err != nil {
90+
fileLock.Unlock()
9091
return "", err
9192
}
9293
fileLock.Unlock()

launchpad/pkg/flagd.go

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package flagd
22

33
import (
4-
"bufio"
54
"context"
65
"errors"
76
"fmt"
8-
"io"
7+
"net/http"
98
"os"
109
"os/exec"
11-
"strings"
1210
"sync"
1311
"time"
1412
)
@@ -95,39 +93,36 @@ func StartFlagd(config string) error {
9593
configPath := fmt.Sprintf("./configs/%s.json", config)
9694

9795
flagdCmd = exec.Command("./flagd", "start", "--config", configPath)
98-
99-
stdout, err := flagdCmd.StdoutPipe()
100-
if err != nil {
101-
return fmt.Errorf("failed to capture stdout: %v", err)
102-
}
103-
stderr, err := flagdCmd.StderrPipe()
104-
if err != nil {
105-
return fmt.Errorf("failed to capture stderr: %v", err)
106-
}
96+
flagdCmd.Stdout = os.Stdout
97+
flagdCmd.Stderr = os.Stderr
10798

10899
if err := flagdCmd.Start(); err != nil {
100+
flagdLock.Unlock()
109101
return fmt.Errorf("failed to start flagd: %v", err)
110102
}
111-
112103
flagdLock.Unlock()
113-
ready := make(chan bool)
114104

115-
go monitorOutput(stdout, ready, "stdout")
116-
go monitorOutput(stderr, ready, "stderr")
105+
// Poll health endpoint until ready
106+
client := &http.Client{Timeout: 500 * time.Millisecond}
107+
ticker := time.NewTicker(100 * time.Millisecond)
108+
defer ticker.Stop()
109+
timeout := time.After(10 * time.Second)
117110

118-
select {
119-
case success := <-ready:
120-
if success {
121-
fmt.Println("flagd started successfully.")
122-
return nil
123-
}
124-
return fmt.Errorf("flagd did not start correctly")
125-
case <-time.After(10 * time.Second):
126-
err := StopFlagd()
127-
if err != nil {
128-
fmt.Println("could not stop flagd", err)
111+
for {
112+
select {
113+
case <-timeout:
114+
_ = StopFlagd()
115+
return fmt.Errorf("flagd health check timed out")
116+
case <-ticker.C:
117+
resp, err := client.Get("http://localhost:8014/readyz")
118+
if err == nil {
119+
resp.Body.Close()
120+
if resp.StatusCode == http.StatusOK {
121+
fmt.Println("flagd started successfully.")
122+
return nil
123+
}
124+
}
129125
}
130-
return fmt.Errorf("flagd start timeout exceeded")
131126
}
132127
}
133128

@@ -174,23 +169,3 @@ func stopFlagDWithoutLock() error {
174169
}
175170
return nil
176171
}
177-
178-
func monitorOutput(pipe io.ReadCloser, ready chan bool, stream string) {
179-
scanner := bufio.NewScanner(pipe)
180-
//adjust the capacity to your need (max characters in line)
181-
const maxCapacity = 512 * 1024
182-
buf := make([]byte, maxCapacity)
183-
scanner.Buffer(buf, maxCapacity)
184-
started := false
185-
186-
for scanner.Scan() {
187-
line := scanner.Text()
188-
fmt.Println("[flagd ", stream, "]:", line)
189-
if ready != nil && !started && strings.Contains(line, "listening at") {
190-
ready <- true
191-
close(ready)
192-
fmt.Println("flagd started properly found logline with 'listening at'")
193-
started = true
194-
}
195-
}
196-
}

launchpad/pkg/json.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,33 @@ import (
1010
"sync"
1111
)
1212

13+
// writes a file atomically using mv
14+
func atomicWriteFile(filename string, data []byte) error {
15+
tmpFile, err := os.CreateTemp(filepath.Dir(filename), ".tmp-*")
16+
if err != nil {
17+
return err
18+
}
19+
tmpName := tmpFile.Name()
20+
21+
if _, err := tmpFile.Write(data); err != nil {
22+
tmpFile.Close()
23+
os.Remove(tmpName)
24+
return err
25+
}
26+
27+
if err := tmpFile.Close(); err != nil {
28+
os.Remove(tmpName)
29+
return err
30+
}
31+
32+
if err := os.Rename(tmpName, filename); err != nil {
33+
os.Remove(tmpName)
34+
return err
35+
}
36+
37+
return nil
38+
}
39+
1340
var (
1441
mu sync.Mutex
1542
InputDir = "./rawflags"
@@ -54,7 +81,7 @@ func CombineJSONFiles(inputDir string) error {
5481
return fmt.Errorf("failed to serialize combined JSON: %v", err)
5582
}
5683

57-
return ioutil.WriteFile(OutputFile, combinedContent, 0644)
84+
return atomicWriteFile(OutputFile, combinedContent)
5885
}
5986

6087
func deepMerge(dst, src map[string]interface{}) map[string]interface{} {

0 commit comments

Comments
 (0)