Skip to content

Commit e0dd999

Browse files
committed
refactor: simplify SessionManager locking and daemon state
1 parent a082c31 commit e0dd999

8 files changed

Lines changed: 79 additions & 99 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ uv.lock
33
**/__pycache__
44
**/*.pyc
55
julia-mcp
6-
/julia-client
6+
/julia-client
7+
go/go

go.mod

Lines changed: 0 additions & 3 deletions
This file was deleted.

go/client_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,12 @@ func TestPkgPattern(t *testing.T) {
9595
// ---- handleRequest (no Julia needed) ----
9696

9797
func newTestState() *daemonState {
98-
return &daemonState{
99-
manager: newSessionManager(),
100-
lastRequest: time.Now(),
101-
stopCh: make(chan struct{}),
98+
s := &daemonState{
99+
manager: newSessionManager(),
100+
stopCh: make(chan struct{}),
102101
}
102+
s.lastRequest.Store(time.Now().UnixNano())
103+
return s
103104
}
104105

105106
func TestHandleRequest_Ping(t *testing.T) {

go/daemon.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,21 @@ import (
1111
"strconv"
1212
"strings"
1313
"sync"
14+
"sync/atomic"
1415
"time"
1516
)
1617

1718
var pkgPattern = regexp.MustCompile(`\bPkg\.`)
1819

1920
type daemonState struct {
2021
manager *SessionManager
21-
mu sync.Mutex
22-
lastRequest time.Time
22+
lastRequest atomic.Int64 // UnixNano
2323
stopOnce sync.Once
2424
stopCh chan struct{}
2525
}
2626

2727
func handleRequest(state *daemonState, req map[string]any) map[string]any {
28-
state.mu.Lock()
29-
state.lastRequest = time.Now()
30-
state.mu.Unlock()
28+
state.lastRequest.Store(time.Now().UnixNano())
3129

3230
action, _ := req["action"].(string)
3331

@@ -144,10 +142,10 @@ func serveDaemon(socketPath string, idleTimeout time.Duration) error {
144142
fmt.Fprintf(os.Stderr, "julia-daemon listening on %s\n", socketPath)
145143

146144
state := &daemonState{
147-
manager: newSessionManager(),
148-
lastRequest: time.Now(),
149-
stopCh: make(chan struct{}),
145+
manager: newSessionManager(),
146+
stopCh: make(chan struct{}),
150147
}
148+
state.lastRequest.Store(time.Now().UnixNano())
151149

152150
// Idle watchdog: closes listener when idle or stop requested
153151
go func() {
@@ -159,9 +157,7 @@ func serveDaemon(socketPath string, idleTimeout time.Duration) error {
159157
ln.Close()
160158
return
161159
case <-ticker.C:
162-
state.mu.Lock()
163-
idle := time.Since(state.lastRequest)
164-
state.mu.Unlock()
160+
idle := time.Since(time.Unix(0, state.lastRequest.Load()))
165161
if idle > idleTimeout {
166162
ln.Close()
167163
return

go/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/Beforerr/julia-client
2+
3+
go 1.25.0
4+
5+
require golang.org/x/sync v0.20.0 // indirect

go/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
2+
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=

go/session.go

Lines changed: 56 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import (
1313
"sync"
1414
"sync/atomic"
1515
"time"
16+
17+
"golang.org/x/sync/singleflight"
1618
)
1719

1820
const (
19-
defaultEvalTimeout = 60.0
20-
startupTimeout = 120.0
21-
tempSessionKey = "__temp__"
21+
defaultEvalTimeout = 60.0
22+
startupTimeout = 120.0
23+
tempSessionKey = "__temp__"
2224
)
2325

2426
// JuliaSession manages a single persistent Julia subprocess.
@@ -244,27 +246,27 @@ func (s *JuliaSession) kill() {
244246
s.proc.Process.Kill()
245247
s.proc.Wait()
246248
}
249+
if s.logFile != nil {
250+
s.logFile.Close()
251+
}
247252
if s.isTemp {
248253
os.RemoveAll(s.envDir)
249254
}
250255
}
251256

252257
// SessionManager tracks multiple named Julia sessions.
253258
type SessionManager struct {
254-
mu sync.Mutex
255-
sessions map[string]*JuliaSession
256-
createLocks map[string]*sync.Mutex
257-
logDir string
258-
logFiles map[string]*os.File
259+
mu sync.Mutex
260+
sessions map[string]*JuliaSession
261+
sf singleflight.Group
262+
logDir string
259263
}
260264

261265
func newSessionManager() *SessionManager {
262266
logDir, _ := os.MkdirTemp("", "julia-client-logs-")
263267
return &SessionManager{
264-
sessions: make(map[string]*JuliaSession),
265-
createLocks: make(map[string]*sync.Mutex),
266-
logDir: logDir,
267-
logFiles: make(map[string]*os.File),
268+
sessions: make(map[string]*JuliaSession),
269+
logDir: logDir,
268270
}
269271
}
270272

@@ -276,88 +278,70 @@ func (m *SessionManager) key(envPath string) string {
276278
return abs
277279
}
278280

279-
func (m *SessionManager) logFile(key string) *os.File {
280-
if f, ok := m.logFiles[key]; ok {
281-
return f
282-
}
281+
func (m *SessionManager) openLogFile(key string) *os.File {
283282
safe := strings.NewReplacer("/", "_", "\\", "_").Replace(strings.Trim(key, "/"))
284283
if safe == "" {
285284
safe = "temp"
286285
}
287-
f, err := os.OpenFile(filepath.Join(m.logDir, safe+".log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
288-
if err != nil {
289-
return nil
290-
}
291-
m.logFiles[key] = f
286+
f, _ := os.OpenFile(filepath.Join(m.logDir, safe+".log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
292287
return f
293288
}
294289

295-
func (m *SessionManager) createLock(key string) *sync.Mutex {
296-
m.mu.Lock()
297-
defer m.mu.Unlock()
298-
if m.createLocks[key] == nil {
299-
m.createLocks[key] = &sync.Mutex{}
300-
}
301-
return m.createLocks[key]
302-
}
303-
304290
func (m *SessionManager) getOrCreate(envPath, juliaCmd string) (*JuliaSession, error) {
305291
key := m.key(envPath)
306292

307-
// Fast path
293+
// Fast path: return existing live session without singleflight overhead.
308294
m.mu.Lock()
309295
sess := m.sessions[key]
310296
m.mu.Unlock()
311297
if sess != nil && sess.isAlive() && sess.juliaCmd == juliaCmd {
312298
return sess, nil
313299
}
314300

315-
// Slow path: serialize creation per key
316-
mu := m.createLock(key)
317-
mu.Lock()
318-
defer mu.Unlock()
319-
320-
m.mu.Lock()
321-
sess = m.sessions[key]
322-
m.mu.Unlock()
323-
if sess != nil && sess.isAlive() && sess.juliaCmd == juliaCmd {
324-
return sess, nil
325-
}
326-
if sess != nil {
327-
sess.kill()
301+
// Slow path: deduplicate concurrent creation for the same key.
302+
v, err, _ := m.sf.Do(key, func() (any, error) {
328303
m.mu.Lock()
329-
delete(m.sessions, key)
304+
sess := m.sessions[key]
330305
m.mu.Unlock()
331-
}
306+
if sess != nil && sess.isAlive() && sess.juliaCmd == juliaCmd {
307+
return sess, nil
308+
}
309+
if sess != nil {
310+
sess.kill()
311+
m.mu.Lock()
312+
delete(m.sessions, key)
313+
m.mu.Unlock()
314+
}
332315

333-
isTemp := envPath == ""
334-
var envDir string
335-
var isTest bool
336-
if isTemp {
337-
var err error
338-
envDir, err = os.MkdirTemp("", "julia-client-")
339-
if err != nil {
340-
return nil, err
316+
isTemp := envPath == ""
317+
var envDir string
318+
var isTest bool
319+
if isTemp {
320+
var err error
321+
envDir, err = os.MkdirTemp("", "julia-client-")
322+
if err != nil {
323+
return nil, err
324+
}
325+
} else {
326+
abs, _ := filepath.Abs(envPath)
327+
envDir = abs
328+
isTest = filepath.Base(envDir) == "test"
341329
}
342-
} else {
343-
abs, _ := filepath.Abs(envPath)
344-
envDir = abs
345-
isTest = filepath.Base(envDir) == "test"
346-
}
347330

348-
m.mu.Lock()
349-
lf := m.logFile(key)
350-
m.mu.Unlock()
331+
sess = newJuliaSession(envDir, newSentinel(), isTemp, isTest, juliaCmd, m.openLogFile(key))
332+
if err := sess.start(); err != nil {
333+
return nil, err
334+
}
351335

352-
sess = newJuliaSession(envDir, newSentinel(), isTemp, isTest, juliaCmd, lf)
353-
if err := sess.start(); err != nil {
336+
m.mu.Lock()
337+
m.sessions[key] = sess
338+
m.mu.Unlock()
339+
return sess, nil
340+
})
341+
if err != nil {
354342
return nil, err
355343
}
356-
357-
m.mu.Lock()
358-
m.sessions[key] = sess
359-
m.mu.Unlock()
360-
return sess, nil
344+
return v.(*JuliaSession), nil
361345
}
362346

363347
func (m *SessionManager) remove(envPath string) {
@@ -390,15 +374,15 @@ func (m *SessionManager) list() []sessionInfo {
390374
m.mu.Lock()
391375
defer m.mu.Unlock()
392376
result := make([]sessionInfo, 0, len(m.sessions))
393-
for key, sess := range m.sessions {
377+
for _, sess := range m.sessions {
394378
info := sessionInfo{
395379
envPath: sess.envDir,
396380
alive: sess.isAlive(),
397381
isTemp: sess.isTemp,
398382
juliaCmd: sess.juliaCmd,
399383
}
400-
if f := m.logFiles[key]; f != nil {
401-
info.logFile = f.Name()
384+
if sess.logFile != nil {
385+
info.logFile = sess.logFile.Name()
402386
}
403387
result = append(result, info)
404388
}
@@ -417,8 +401,5 @@ func (m *SessionManager) shutdown() {
417401
for _, s := range sessions {
418402
s.kill()
419403
}
420-
for _, f := range m.logFiles {
421-
f.Close()
422-
}
423404
os.RemoveAll(m.logDir)
424405
}

justfile

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
install:
22
julia-client stop
3-
go build -o ~/.local/bin/julia-client ./go/
3+
go build -C go -o ~/.local/bin/julia-client .
44
npx skills add . -g -y
55

66
test:
7-
go test -v -timeout 300s ./go/
8-
9-
build:
10-
go build -o julia-client ./go/
7+
go test -C go -v -timeout 300s ./...
118

129
release version="":
1310
#!/usr/bin/env bash

0 commit comments

Comments
 (0)