Skip to content

perf: per-session mutex in WriteStatsDetailed#12

Merged
pythondatascrape merged 1 commit into
mainfrom
fix/per-session-mutex
May 6, 2026
Merged

perf: per-session mutex in WriteStatsDetailed#12
pythondatascrape merged 1 commit into
mainfrom
fix/per-session-mutex

Conversation

@pythondatascrape
Copy link
Copy Markdown
Owner

Closes #10

Summary

  • Replaced single global mu sync.Mutex with a per-session-ID map of mutexes
  • Writes to different sessions no longer block each other
  • Same-session writes still serialize correctly via the per-session lock

Test plan

  • Existing TestWriteStats_* and TestWriteStatsDetailed_* tests pass
  • New TestWriteStats_ConcurrentDifferentSessions verifies concurrent writes to distinct sessions all succeed with correct totals

…iled

Closes #10

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 6, 2026 14:19
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the proxy session stats writer to reduce cross-session contention during .ctx.json updates by moving from a single process-wide mutex to per-session locking, and expands the written stats structure/tests to support richer per-turn/category analytics.

Changes:

  • Replace the single global stats write mutex with a per-session-ID lock map to allow parallel writes across different sessions.
  • Introduce WriteStatsDetailed plus new on-disk fields (per_turn, total, identity_tokens, context_tokens) for more detailed session analytics.
  • Update/add tests to validate the new schema and concurrent writing behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
internal/proxy/session.go Implements per-session locking and extends the ctx stats schema + adds WriteStatsDetailed.
internal/proxy/session_test.go Updates tests for the new schema and adds concurrent multi-session write coverage.
Comments suppressed due to low confidence (1)

internal/proxy/session.go:104

  • sessionID is concatenated directly into a filename (filepath.Join(sessionsDir, sessionID+".ctx.json")). Because sessionID can come from the X-Engram-Session header, a value containing path separators (e.g. "../") can escape sessionsDir and overwrite arbitrary files. Restrict sessionID to a safe character set (or use filepath.Base + explicit rejection of path separators) before using it in filesystem paths.
	if err := os.MkdirAll(sessionsDir, 0o700); err != nil {
		return fmt.Errorf("create sessions dir: %w", err)
	}

	path := filepath.Join(sessionsDir, sessionID+".ctx.json")


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/proxy/session.go
Comment on lines +13 to +33
// sessionMu provides per-session-ID locking so concurrent writes to different
// sessions do not block each other.
var sessionMu struct {
sync.Mutex
m map[string]*sync.Mutex
}

func sessionLock(id string) func() {
sessionMu.Lock()
if sessionMu.m == nil {
sessionMu.m = make(map[string]*sync.Mutex)
}
mu, ok := sessionMu.m[id]
if !ok {
mu = &sync.Mutex{}
sessionMu.m[id] = mu
}
sessionMu.Unlock()
mu.Lock()
return mu.Unlock
}
Comment thread internal/proxy/session.go
Comment on lines +48 to +51
PerTurn tokenTotals `json:"per_turn,omitempty"`
Total tokenTotals `json:"total,omitempty"`
Identity tokenSeries `json:"identity_tokens,omitempty"`
Context tokenSeries `json:"context_tokens,omitempty"`
Comment thread internal/proxy/session.go
Comment on lines 43 to +52
// ctxStats is the on-disk structure for proxy-measured context token accounting.
type ctxStats struct {
CtxOrig int `json:"ctx_orig"`
CtxComp int `json:"ctx_comp"`
Turns int `json:"turns"`
CtxOrig int `json:"ctx_orig"`
CtxComp int `json:"ctx_comp"`
Turns int `json:"turns"`
PerTurn tokenTotals `json:"per_turn,omitempty"`
Total tokenTotals `json:"total,omitempty"`
Identity tokenSeries `json:"identity_tokens,omitempty"`
Context tokenSeries `json:"context_tokens,omitempty"`
}
Comment thread internal/proxy/session.go
Comment on lines +110 to +117
stats.CtxOrig += turn.Total.Orig
stats.CtxComp += turn.Total.Comp
stats.Turns++
stats.PerTurn = tokenTotals{
Orig: turn.Total.Orig,
Comp: turn.Total.Comp,
Saved: clampSaved(turn.Total.Orig, turn.Total.Comp),
}
Comment on lines +118 to +122
for _, key := range []string{"ctx_orig", "ctx_comp", "turns", "per_turn", "total", "identity_tokens", "context_tokens"} {
if _, ok := got[key]; !ok {
t.Errorf("ctx file missing key %q; got keys: %v", key, got)
}
}
Comment on lines +164 to +179
// TestWriteStats_PerSessionLocking verifies that writes to different sessions
// do not block each other — distinct sessions must hold independent locks.
func TestWriteStats_PerSessionLocking(t *testing.T) {
dir := t.TempDir()
const sessions = 20
done := make(chan error, sessions)
for i := 0; i < sessions; i++ {
go func(n int) {
done <- WriteStats(dir, fmt.Sprintf("session-%d", n), n*100, n*30)
}(i)
}
for i := 0; i < sessions; i++ {
if err := <-done; err != nil {
t.Errorf("parallel WriteStats failed: %v", err)
}
}
@pythondatascrape pythondatascrape merged commit bce2e76 into main May 6, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: global mutex in WriteStatsDetailed serialises all session writes

2 participants