First commit Go counter service
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user