Skip to content

Commit 3ab95a4

Browse files
author
Paulo Gomes
committed
libgit2: close discarded connections
Cached connections can be shared across concurrent operations, and their disposal must take that into account to avoid closing a connection that is stale for one goroutine, but is still valid for another. Signed-off-by: Paulo Gomes <[email protected]>
1 parent add0774 commit 3ab95a4

File tree

1 file changed

+59
-6
lines changed

1 file changed

+59
-6
lines changed

pkg/git/libgit2/managed/ssh.go

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,19 @@ type sshSmartSubtransport struct {
107107
// aMux is the read-write mutex to control access to sshClients.
108108
var 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

119124
func (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

351362
func (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

355373
func 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

Comments
 (0)