First commit Go counter service

This commit is contained in:
Dimitrije Dimitrijevic
2026-10-15 02:12:18 +02:00
commit c43ff17627
59 changed files with 11205 additions and 0 deletions
@@ -0,0 +1,34 @@
package counting
import (
"path/filepath"
"testing"
"time"
"github.com/colinmarc/cdb"
"migadu_counters/internal/domains"
)
func TestReview_MalformedRowFailsWholeRequest(t *testing.T) {
path := filepath.Join(t.TempDir(), "d.cdb")
w, err := cdb.Create(path)
if err != nil {
t.Fatal(err)
}
w.Put([]byte("good.example"), []byte("7|A/o=5,A/i=5"))
w.Put([]byte("bad.example"), []byte("|A/i=5")) // org id missing (malformed row)
if _, err := w.Freeze(); err != nil {
t.Fatal(err)
}
db, err := domains.Open(path)
if err != nil {
t.Fatal(err)
}
defer db.Close()
p := &Planner{Lookup: db, Now: time.Now}
plan, err := p.PlanOut("t", "x@good.example", "x@good.example", "y@bad.example")
t.Logf("PlanOut(good sender -> malformed-row recipient): plan=%v err=%v", plan, err)
plan, err = p.PlanIn("t", "s@gmail.com", "y@bad.example", false)
t.Logf("PlanIn(malformed-row recipient): plan=%v err=%v", plan, err)
}
@@ -0,0 +1,96 @@
package server
import (
"fmt"
"net"
"strings"
"testing"
"time"
"migadu_counters/internal/counting"
"migadu_counters/internal/domains"
)
func rawPostAndClose(t *testing.T, addr, path, body string) {
c, err := net.Dial("tcp", addr)
if err != nil {
t.Fatal(err)
}
req := fmt.Sprintf("POST %s HTTP/1.1\r\nHost: x\r\nAuthorization: Bearer %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", path, token, len(body), body)
if _, err := c.Write([]byte(req)); err != nil {
t.Fatal(err)
}
c.Close() // fire-and-forget client that does not wait for the reply
}
func TestReview_ClientDisconnectLosesCount(t *testing.T) {
h := newHarness(t, options{})
real := *h.lookup.Load()
slow := counting.LookupFunc(func(d string) (*domains.Domain, error) {
time.Sleep(50 * time.Millisecond) // any latency before the Redis call
return real(d)
})
h.lookup.Store(&slow)
addr := strings.TrimPrefix(h.api.URL, "http://")
const n = 20
for i := 0; i < n; i++ {
rawPostAndClose(t, addr, "/in", `{"trace_id":"t","mail_from":"s@gmail.com","recipient":"a@yagolivas.com"}`)
}
time.Sleep(1500 * time.Millisecond)
v, _ := h.write.Get("cnt/A/a@yagolivas.com/d/" + day + "/i")
t.Logf("sent %d /in requests, counter = %q", n, v)
t.Logf("txlog tail:\n%s", lastLines(h.tx.String(), 3))
}
func TestReview_ClientDisconnectNoDelay(t *testing.T) {
h := newHarness(t, options{})
addr := strings.TrimPrefix(h.api.URL, "http://")
const n = 300
for i := 0; i < n; i++ {
rawPostAndClose(t, addr, "/in", `{"trace_id":"t","mail_from":"s@gmail.com","recipient":"b@yagolivas.com"}`)
}
time.Sleep(2 * time.Second)
v, _ := h.write.Get("cnt/A/b@yagolivas.com/d/" + day + "/i")
t.Logf("sent %d /in requests without delay, counter = %q", n, v)
}
func lastLines(s string, n int) string {
l := strings.Split(strings.TrimRight(s, "\n"), "\n")
if len(l) > n {
l = l[len(l)-n:]
}
return strings.Join(l, "\n")
}
func TestReview_MasterLoginAndOddSenders(t *testing.T) {
h := newHarness(t, options{})
cases := []struct{ path, body string }{
{"/out", `{"trace_id":"m1","auth_user":"x@yagolivas.com@SUPPORTTOKEN","mail_from":"x@yagolivas.com","recipient":"y@yagolivas.com"}`},
{"/in", `{"trace_id":"i1","mail_from":"a b@spammer.example","recipient":"y@yagolivas.com"}`},
{"/in", `{"trace_id":"i2","mail_from":"u@[192.0.2.1]","recipient":"y@yagolivas.com"}`},
{"/in", `{"trace_id":"i3","mail_from":"u@münchen.de","recipient":"y@yagolivas.com"}`},
{"/in", `{"trace_id":"i4","mail_from":"u@example.com.","recipient":"y@yagolivas.com"}`},
{"/in", `{"trace_id":"i5","mail_from":"a@b@example.com","recipient":"y@yagolivas.com"}`},
}
for _, c := range cases {
st, body := h.post(c.path, c.body)
t.Logf("%s %s -> %d %s", c.path, c.body, st, strings.TrimSpace(body))
}
t.Logf("keys in redis: %v", h.write.Keys())
}
func TestReview_PartialExecFailure(t *testing.T) {
h := newHarness(t, options{})
// A counter key holding a non-integer (or wrong type) value.
h.write.Set("cnt/O/1049/d/"+day+"/o", "garbage")
// A/o limit is 20 for yagolivas.com; preset A/o to 19 so this request crosses it.
h.write.Set("cnt/A/x@yagolivas.com/d/"+day+"/o", "19")
st, body := h.post("/out", `{"trace_id":"p1","auth_user":"x@yagolivas.com","recipient":"z@gmail.com"}`)
t.Logf("status %d body %s", st, strings.TrimSpace(body))
a, _ := h.write.Get("cnt/A/x@yagolivas.com/d/" + day + "/o")
d, _ := h.write.Get("cnt/D/yagolivas.com/d/" + day + "/o")
t.Logf("after: A/o=%s (limit 20 crossed) D/o=%s", a, d)
time.Sleep(300 * time.Millisecond)
t.Logf("address notifications sent: %d", h.addrURL.count())
t.Logf("txlog:\n%s", h.tx.String())
}
@@ -0,0 +1,36 @@
package store
import (
"context"
"testing"
"time"
"github.com/redis/go-redis/v9"
"migadu_counters/internal/config"
)
func TestReview_TimeoutAfterWriteStillCounts(t *testing.T) {
st, err := New(config.RedisConfig{WriteAddr: "127.0.0.1:16401", PoolSize: 4, TimeoutMS: 500})
if err != nil {
t.Fatal(err)
}
defer st.Close()
admin := redis.NewClient(&redis.Options{Addr: "127.0.0.1:16401"})
defer admin.Close()
key := "cnt/A/review@example.com/d/2026-10-14/o"
admin.Del(context.Background(), key)
admin.Set(context.Background(), key, "99", 0) // limit 100: next increment crosses
// warm the pool
if err := st.Ping(context.Background()); err != nil {
t.Fatal(err)
}
go admin.Do(context.Background(), "DEBUG", "SLEEP", "1") // server stall > timeout_ms
time.Sleep(50 * time.Millisecond)
_, err = st.Incr(context.Background(), []string{key}, 1)
t.Logf("Incr error: %v", err)
time.Sleep(1500 * time.Millisecond)
v, _ := admin.Get(context.Background(), key).Result()
t.Logf("key after the 'failed' Incr: %s", v)
admin.Del(context.Background(), key)
}
@@ -0,0 +1,63 @@
package txlog
import (
"io"
"testing"
"time"
"migadu_counters/internal/counting"
"migadu_counters/internal/domains"
)
const cdbPath = "../../test_files/rspamd_domains_db.cdb"
func BenchmarkReview_Open(b *testing.B) {
for i := 0; i < b.N; i++ {
db, err := domains.Open(cdbPath)
if err != nil {
b.Fatal(err)
}
db.Close()
}
}
func benchLookup(b *testing.B, dom string) {
db, err := domains.Open(cdbPath)
if err != nil {
b.Fatal(err)
}
defer db.Close()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := db.Lookup(dom); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkReview_LookupSmall(b *testing.B) { benchLookup(b, "yagolivas.com") }
func BenchmarkReview_LookupHuge(b *testing.B) { benchLookup(b, "boxmailhere.com") }
func BenchmarkReview_InRequestHuge(b *testing.B) {
db, err := domains.Open(cdbPath)
if err != nil {
b.Fatal(err)
}
defer db.Close()
p := &counting.Planner{Lookup: db, Now: time.Now}
l := New(io.Discard, "text", nil)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
plan, err := p.PlanIn("t", "s@gmail.com", "someone@boxmailhere.com", false)
if err != nil {
b.Fatal(err)
}
out := &counting.Outcome{Plan: plan}
for _, inc := range plan.Increments {
out.Results = append(out.Results, counting.Result{Increment: inc, Count: 1})
}
l.Outcome(out, nil)
}
}