From 2bcedd240df7d909a86977b8f5ed99bf51c936d6 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 30 Apr 2026 09:21:36 +0000 Subject: [PATCH] feat(audit): add per-session sequence counter Introduce SequenceCounter, a shared atomic uint64 counter intended to be initialized once per boundary session and shared between the socket auditor and the proxy. The Next() method returns a zero-based, monotonically increasing value so the audit event and any injected header for the same request agree on the sequence number. No wiring yet; this commit only adds the type and its tests. --- audit/sequence.go | 20 ++++++++++ audit/sequence_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 audit/sequence.go create mode 100644 audit/sequence_test.go diff --git a/audit/sequence.go b/audit/sequence.go new file mode 100644 index 0000000..1735d49 --- /dev/null +++ b/audit/sequence.go @@ -0,0 +1,20 @@ +package audit + +import "sync/atomic" + +// SequenceCounter is a monotonically increasing counter that assigns a +// unique sequence number to every audit event within a single boundary +// session. The counter starts at 0 and is safe for concurrent use by +// both the socket auditor and the proxy. +type SequenceCounter struct { + next atomic.Uint64 +} + +// Next returns the next sequence number. The first call returns 0, +// subsequent calls return 1, 2, 3, etc. It is safe for concurrent +// use. +func (c *SequenceCounter) Next() uint64 { + // Add returns the new value after incrementing, so subtract 1 + // to produce a zero-based sequence. + return c.next.Add(1) - 1 +} diff --git a/audit/sequence_test.go b/audit/sequence_test.go new file mode 100644 index 0000000..e0231e2 --- /dev/null +++ b/audit/sequence_test.go @@ -0,0 +1,87 @@ +package audit + +import ( + "sync" + "testing" +) + +func TestSequenceCounter_StartsAtZero(t *testing.T) { + t.Parallel() + + var c SequenceCounter + if got := c.Next(); got != 0 { + t.Fatalf("first call: got %d, want 0", got) + } +} + +func TestSequenceCounter_Increments(t *testing.T) { + t.Parallel() + + var c SequenceCounter + for i := range uint64(100) { + if got := c.Next(); got != i { + t.Fatalf("call %d: got %d, want %d", i, got, i) + } + } +} + +func TestSequenceCounter_ConcurrentAccess(t *testing.T) { + t.Parallel() + + var c SequenceCounter + + const goroutines = 8 + const callsPerGoroutine = 1000 + const total = goroutines * callsPerGoroutine + + seen := make([]bool, total) + var mu sync.Mutex + + var wg sync.WaitGroup + wg.Add(goroutines) + for range goroutines { + go func() { + defer wg.Done() + for range callsPerGoroutine { + n := c.Next() + mu.Lock() + if n >= total { + mu.Unlock() + t.Errorf("sequence number %d out of range [0, %d)", n, total) + return + } + if seen[n] { + mu.Unlock() + t.Errorf("duplicate sequence number %d", n) + return + } + seen[n] = true + mu.Unlock() + } + }() + } + + wg.Wait() + + for i, ok := range seen { + if !ok { + t.Errorf("sequence number %d was never produced", i) + } + } +} + +func TestSequenceCounter_IndependentInstances(t *testing.T) { + t.Parallel() + + var a, b SequenceCounter + + // Advance a a few times. + for range 5 { + a.Next() + } + + // b should still start at 0. + if got := b.Next(); got != 0 { + t.Fatalf("independent counter: got %d, want 0", got) + } +}