@@ -13,12 +13,14 @@ import (
1313 "sync"
1414 "sync/atomic"
1515 "time"
16+
17+ "golang.org/x/sync/singleflight"
1618)
1719
1820const (
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.
253258type 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
261265func 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-
304290func (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
363347func (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}
0 commit comments