返回 DeepSeek-Reasonix
pending.go
根目录 / internal / telemetry / pending.go
1 package telemetry
2
3 import (
4 "crypto/rand"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "os"
10 "path/filepath"
11 "sort"
12 "strings"
13 "time"
14 )
15
16 const (
17 pendingDirName = "cli-telemetry-pending"
18 maxPending = 64
19 maxPendingAge = 14 * 24 * time.Hour
20 )
21
22 func Cleanup(home string) error {
23 if strings.TrimSpace(home) == "" {
24 return nil
25 }
26 err := os.RemoveAll(filepath.Join(home, pendingDirName))
27 if errors.Is(err, os.ErrNotExist) {
28 return nil
29 }
30 return err
31 }
32
33 func appendPending(home string, p pendingPayload) error {
34 if len(p.Counters) == 0 || strings.TrimSpace(home) == "" {
35 return nil
36 }
37 dir := filepath.Join(home, pendingDirName)
38 if err := os.MkdirAll(dir, 0o700); err != nil {
39 return err
40 }
41 if !prunePending(dir, time.Now()) {
42 return nil
43 }
44 b, err := json.Marshal(p)
45 if err != nil {
46 return err
47 }
48 nonce, err := randomHex(8)
49 if err != nil {
50 return err
51 }
52 name := fmt.Sprintf("%d-%d-%s.json", time.Now().UnixNano(), os.Getpid(), nonce)
53 tmp := filepath.Join(dir, "."+name+".tmp")
54 if err := os.WriteFile(tmp, b, 0o600); err != nil {
55 return err
56 }
57 if err := os.Rename(tmp, filepath.Join(dir, name)); err != nil {
58 _ = os.Remove(tmp)
59 return err
60 }
61 return nil
62 }
63
64 // prunePending removes expired entries and makes room for one new pending file.
65 // Active upload claims count toward the cap but are never removed; if every
66 // slot is actively claimed, the new sample is dropped instead of growing the
67 // queue without bound.
68 func prunePending(dir string, now time.Time) bool {
69 entries, err := os.ReadDir(dir)
70 if err != nil {
71 return false
72 }
73 type item struct {
74 path string
75 mod time.Time
76 }
77 items := make([]item, 0, len(entries))
78 activeClaims := 0
79 for _, entry := range entries {
80 if entry.IsDir() || (!strings.HasSuffix(entry.Name(), ".json") && !strings.HasSuffix(entry.Name(), ".json.uploading")) {
81 continue
82 }
83 info, err := entry.Info()
84 if err != nil {
85 continue
86 }
87 path := filepath.Join(dir, entry.Name())
88 if now.Sub(info.ModTime()) > maxPendingAge {
89 _ = os.Remove(path)
90 continue
91 }
92 if strings.HasSuffix(path, ".json.uploading") {
93 if now.Sub(info.ModTime()) < 2*time.Minute {
94 activeClaims++
95 continue
96 }
97 recovered := strings.TrimSuffix(path, ".uploading")
98 if err := os.Rename(path, recovered); err != nil {
99 activeClaims++
100 continue
101 }
102 path = recovered
103 }
104 items = append(items, item{path: path, mod: info.ModTime()})
105 }
106 sort.Slice(items, func(i, j int) bool { return items[i].mod.Before(items[j].mod) })
107 for len(items)+activeClaims >= maxPending && len(items) > 0 {
108 _ = os.Remove(items[0].path)
109 items = items[1:]
110 }
111 return len(items)+activeClaims < maxPending
112 }
113
114 func randomHex(bytes int) (string, error) {
115 b := make([]byte, bytes)
116 if _, err := rand.Read(b); err != nil {
117 return "", err
118 }
119 return hex.EncodeToString(b), nil
120 }
121
121 lines GO