carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package relay
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8 "testing"
9 "time"
10)
11
12func TestMeterFlushAppendsDeltas(t *testing.T) {
13 s := newMemStore(t)
14 now := time.Date(2026, 9, 7, 12, 30, 0, 0, time.UTC)
15 m := NewMeter("box1", func() time.Time { return now }, nil)
16 id := Identity{"abc", "meet"}
17 m.Add(id, 100, 200, 5)
18 if err := m.Flush(context.Background(), s); err != nil {
19 t.Fatal(err)
20 }
21 m.Add(id, 10, 20, 0)
22 if err := m.Flush(context.Background(), s); err != nil {
23 t.Fatal(err)
24 }
25 body, _, _ := s.Get(context.Background(), MeterKey("box1", "2026-09-07-12"))
26 var c MeterChunk
27 json.Unmarshal(body, &c)
28 if len(c.Samples) != 2 || c.Samples[0].Apps["abc/meet"].In != 100 || c.Samples[1].Apps["abc/meet"].Out != 20 {
29 t.Fatalf("chunk = %+v", c)
30 }
31}
32
33func TestLoadWindowSpansTwoHours(t *testing.T) {
34 s := newMemStore(t)
35 now := time.Date(2026, 9, 7, 12, 1, 0, 0, time.UTC)
36 m := NewMeter("box1", func() time.Time { return now.Add(-30 * time.Minute) }, nil) // 11:31
37 m.Add(Identity{"abc", "meet"}, 7, 0, 0)
38 m.Flush(context.Background(), s)
39 samples, err := LoadWindow(context.Background(), s, "box1", now, nil)
40 if err != nil || len(samples) != 1 || samples[0].Apps["abc/meet"].In != 7 {
41 t.Fatalf("window = %+v %v", samples, err)
42 }
43}
44
45// TestMeterFlushPreservesCorruptChunk checks that a Flush landing on a
46// chunk that fails to unmarshal doesn't just clobber it: the unreadable
47// bytes are preserved as a ".corrupt-<unix>" sibling, a fresh chunk is
48// written in the real key's place, and the corruption is logged.
49func TestMeterFlushPreservesCorruptChunk(t *testing.T) {
50 s := newMemStore(t)
51 now := time.Date(2026, 9, 7, 12, 30, 0, 0, time.UTC)
52 key := MeterKey("box1", "2026-09-07-12")
53 if _, err := s.Put(context.Background(), key, []byte("not json"), ""); err != nil {
54 t.Fatal(err)
55 }
56
57 var logs []string
58 m := NewMeter("box1", func() time.Time { return now }, func(format string, args ...any) {
59 logs = append(logs, fmt.Sprintf(format, args...))
60 })
61 m.Add(Identity{"abc", "meet"}, 1, 2, 0)
62 if err := m.Flush(context.Background(), s); err != nil {
63 t.Fatal(err)
64 }
65
66 body, _, err := s.Get(context.Background(), key)
67 if err != nil {
68 t.Fatal(err)
69 }
70 var c MeterChunk
71 if err := json.Unmarshal(body, &c); err != nil {
72 t.Fatalf("chunk at %s still unreadable: %v", key, err)
73 }
74 if len(c.Samples) != 1 {
75 t.Fatalf("chunk = %+v", c)
76 }
77
78 keys, err := s.List(context.Background(), "metrics/relay/box1/")
79 if err != nil {
80 t.Fatal(err)
81 }
82 found := false
83 for _, k := range keys {
84 if strings.Contains(k, ".corrupt-") {
85 found = true
86 }
87 }
88 if !found {
89 t.Fatalf("no .corrupt- sibling among %v", keys)
90 }
91 if len(logs) == 0 {
92 t.Fatal("expected a log message about the corrupt chunk")
93 }
94}
95
96// TestMeterEvictsDeadApps checks that an app with nothing left to report
97// (zero deltas, zero allocations) drops out of the meter's live set — and
98// out of the sample — after being flushed once, while an app still holding
99// open allocations keeps being reported even with zero deltas.
100func TestMeterEvictsDeadApps(t *testing.T) {
101 s := newMemStore(t)
102 now := time.Date(2026, 9, 7, 12, 30, 0, 0, time.UTC)
103 m := NewMeter("box1", func() time.Time { return now }, nil)
104 a := Identity{"abc", "meet"}
105 b := Identity{"abc", "docs"}
106 m.Add(a, 1, 2, 0)
107 m.Add(b, 3, 4, 0)
108 m.SetAllocs(a, 1)
109 m.SetAllocs(b, 0)
110
111 if err := m.Flush(context.Background(), s); err != nil {
112 t.Fatal(err)
113 }
114 if err := m.Flush(context.Background(), s); err != nil {
115 t.Fatal(err)
116 }
117
118 body, _, err := s.Get(context.Background(), MeterKey("box1", "2026-09-07-12"))
119 if err != nil {
120 t.Fatal(err)
121 }
122 var c MeterChunk
123 if err := json.Unmarshal(body, &c); err != nil {
124 t.Fatal(err)
125 }
126 if len(c.Samples) != 2 {
127 t.Fatalf("chunk = %+v", c)
128 }
129
130 second := c.Samples[1]
131 appA, ok := second.Apps["abc/meet"]
132 if !ok || appA.Allocs != 1 || appA.In != 0 || appA.Out != 0 || appA.Rejected != 0 {
133 t.Fatalf("second sample abc/meet = %+v ok=%v", appA, ok)
134 }
135 if _, ok := second.Apps["abc/docs"]; ok {
136 t.Fatalf("second sample still has dead app abc/docs: %+v", second.Apps)
137 }
138}
139
140// TestMeterFlushOfAnIdleRelayWritesNothing: a sample naming no app carries
141// no information, and a relay box is idle almost all of the time — left
142// unguarded, the flush loop writes 1,440 empty rows a day per box, each one
143// a read, a write, a CAS and a row every reader of the hour has to walk. An
144// hour in which nothing happened is best represented by no object at all.
145func TestMeterFlushOfAnIdleRelayWritesNothing(t *testing.T) {
146 s := newMemStore(t)
147 now := time.Date(2026, 9, 7, 12, 30, 0, 0, time.UTC)
148 m := NewMeter("box1", func() time.Time { return now }, nil)
149 for i := 0; i < 2; i++ {
150 if err := m.Flush(context.Background(), s); err != nil {
151 t.Fatalf("flush %d: %v", i, err)
152 }
153 }
154 keys, err := s.List(context.Background(), "metrics/relay/box1/")
155 if err != nil {
156 t.Fatal(err)
157 }
158 if len(keys) != 0 {
159 t.Fatalf("an idle relay wrote %v, want nothing", keys)
160 }
161 // And the first byte that IS forwarded still lands, so this is a skip,
162 // not a mute.
163 m.Add(Identity{"abc", "meet"}, 1, 0, 0)
164 if err := m.Flush(context.Background(), s); err != nil {
165 t.Fatal(err)
166 }
167 if keys, _ := s.List(context.Background(), "metrics/relay/box1/"); len(keys) != 1 {
168 t.Fatalf("after traffic, keys = %v, want the hour's one chunk", keys)
169 }
170}
171
172// TestSetAllocsAllZeroesDepartedApps is the leak test. SetAllocs alone can
173// only ever raise a gauge, because the ids that need lowering are precisely
174// the ones a census of live allocations no longer names — so an app whose
175// last call ended would report a phantom allocation in every sample from
176// then on and would never be evicted, growing m.cur without bound in a
177// process the unit caps at 256 MiB.
178func TestSetAllocsAllZeroesDepartedApps(t *testing.T) {
179 s := newMemStore(t)
180 now := time.Date(2026, 9, 7, 12, 30, 0, 0, time.UTC)
181 m := NewMeter("box1", func() time.Time { return now }, nil)
182 a := Identity{"abc", "meet"}
183 b := Identity{"abc", "docs"}
184
185 // Both live and forwarding.
186 m.Add(a, 10, 20, 0)
187 m.Add(b, 30, 40, 0)
188 m.SetAllocsAll(map[Identity]int{a: 1, b: 2})
189 if err := m.Flush(context.Background(), s); err != nil {
190 t.Fatal(err)
191 }
192 // b's call ends part way through the next minute: it forwarded some
193 // bytes and then went, so the census no longer names it.
194 m.Add(b, 5, 5, 0)
195 m.SetAllocsAll(map[Identity]int{a: 1})
196 if err := m.Flush(context.Background(), s); err != nil {
197 t.Fatal(err)
198 }
199 // And a third flush finds nothing left to say about b at all.
200 m.SetAllocsAll(map[Identity]int{a: 1})
201 if err := m.Flush(context.Background(), s); err != nil {
202 t.Fatal(err)
203 }
204
205 body, _, err := s.Get(context.Background(), MeterKey("box1", "2026-09-07-12"))
206 if err != nil {
207 t.Fatal(err)
208 }
209 var c MeterChunk
210 if err := json.Unmarshal(body, &c); err != nil {
211 t.Fatal(err)
212 }
213 if len(c.Samples) != 3 {
214 t.Fatalf("want 3 samples, got %+v", c.Samples)
215 }
216 if got := c.Samples[0].Apps["abc/docs"].Allocs; got != 2 {
217 t.Errorf("first sample abc/docs allocs = %d, want 2", got)
218 }
219 // The gauge came back DOWN. Without the census this would still read 2,
220 // which is the whole finding.
221 if got, ok := c.Samples[1].Apps["abc/docs"]; !ok || got.Allocs != 0 || got.In != 5 {
222 t.Errorf("second sample abc/docs = %+v ok=%v, want its last 5 bytes with allocs 0", got, ok)
223 }
224 if _, ok := c.Samples[2].Apps["abc/docs"]; ok {
225 t.Errorf("third sample still carries the departed app: %+v", c.Samples[2].Apps)
226 }
227 if got := c.Samples[2].Apps["abc/meet"].Allocs; got != 1 {
228 t.Errorf("third sample abc/meet allocs = %d, want the live app kept at 1", got)
229 }
230 // And it is out of the meter's live set, not merely out of the sample:
231 // the gauge that never fell was a leak as much as a lie.
232 m.mu.Lock()
233 n := len(m.cur)
234 m.mu.Unlock()
235 if n != 1 {
236 t.Errorf("meter still carries %d apps, want only the live one", n)
237 }
238}
239