@@ -107,14 +107,19 @@ type sshSmartSubtransport struct {
107107// aMux is the read-write mutex to control access to sshClients.
108108var aMux sync.RWMutex
109109
110+ type cachedClient struct {
111+ * ssh.Client
112+ activeSessions uint16
113+ }
114+
110115// sshClients stores active ssh clients/connections to be reused.
111116//
112117// Once opened, connections will be kept cached until an error occurs
113118// during SSH commands, by which point it will be discarded, leading to
114119// a follow-up cache miss.
115120//
116121// The key must be based on cacheKey, refer to that function's comments.
117- var sshClients map [string ]* ssh. Client = make (map [string ]* ssh. Client )
122+ var sshClients map [string ]* cachedClient = make (map [string ]* cachedClient )
118123
119124func (t * sshSmartSubtransport ) Action (urlString string , action git2go.SmartServiceAction ) (git2go.SmartSubtransportStream , error ) {
120125 runtime .LockOSThread ()
@@ -202,13 +207,14 @@ func (t *sshSmartSubtransport) Action(urlString string, action git2go.SmartServi
202207 }
203208
204209 var cacheHit bool
205- aMux .RLock ()
210+ aMux .Lock ()
206211 if c , ok := sshClients [ckey ]; ok {
207212 traceLog .Info ("[ssh]: cache hit" , "remoteAddress" , addr )
208- t .client = c
213+ t .client = c . Client
209214 cacheHit = true
215+ c .activeSessions ++
210216 }
211- aMux .RUnlock ()
217+ aMux .Unlock ()
212218
213219 if t .client == nil {
214220 cacheHit = false
@@ -298,7 +304,11 @@ func (t *sshSmartSubtransport) createConn(ckey, addr string, sshConfig *ssh.Clie
298304 return err
299305 }
300306
301- sshClients [ckey ] = t .client
307+ sshClients [ckey ] = & cachedClient {
308+ Client : t .client ,
309+ activeSessions : 1 ,
310+ }
311+
302312 return nil
303313}
304314
@@ -325,6 +335,7 @@ func (t *sshSmartSubtransport) Close() error {
325335 // failure closing a session suggests a stale connection.
326336 if err != nil && t .ckey != "" {
327337 discardCachedSshClient (t .ckey )
338+ t .ckey = ""
328339 }
329340 }
330341 t .session = nil
@@ -350,6 +361,13 @@ func (stream *sshSmartSubtransportStream) Write(buf []byte) (int, error) {
350361
351362func (stream * sshSmartSubtransportStream ) Free () {
352363 traceLog .Info ("[ssh]: sshSmartSubtransportStream.Free()" )
364+ if stream .owner == nil {
365+ return
366+ }
367+
368+ if stream .owner .ckey != "" {
369+ decrementActiveSessionIfFound (stream .owner .ckey )
370+ }
353371}
354372
355373func cacheKeyAndConfig (remoteAddress string , cred * git2go.Credential ) (string , * ssh.ClientConfig , error ) {
@@ -420,8 +438,43 @@ func discardCachedSshClient(key string) {
420438 aMux .Lock ()
421439 defer aMux .Unlock ()
422440
423- if _ , found := sshClients [key ]; found {
441+ if v , found := sshClients [key ]; found {
424442 traceLog .Info ("[ssh]: discard cached ssh client" )
443+
444+ v .activeSessions --
445+ closeConn := func () {
446+ if v .Client != nil {
447+ // run as async goroutine to minimise mutex time in immediate closures.
448+ go func () {
449+ _ = v .Client .Close ()
450+ }()
451+ }
452+ }
453+
454+ // if no active sessions for this connection, close it right-away.
455+ // otherwise, it may be used by other processes, so remove from cache,
456+ // and schedule a delayed closure.
457+ if v .activeSessions == 0 {
458+ traceLog .Info ("[ssh]: closing connection" )
459+ closeConn ()
460+ } else {
461+ go func () {
462+ // the delay must account for in-flight operations
463+ // that depends on this connection.
464+ time .Sleep (120 * time .Second )
465+ traceLog .Info ("[ssh]: closing connection after delay" )
466+ closeConn ()
467+ }()
468+ }
425469 delete (sshClients , key )
426470 }
427471}
472+
473+ func decrementActiveSessionIfFound (key string ) {
474+ aMux .Lock ()
475+ defer aMux .Unlock ()
476+
477+ if v , found := sshClients [key ]; found {
478+ v .activeSessions --
479+ }
480+ }
0 commit comments