Skip to content

Commit 68eece4

Browse files
author
Paulo Gomes
committed
libgit2: optimise mutex on cached connections
Previously the mutex.Lock was acquired before creating a new connection. The lock would then hold until the process was finished, and all network latency would be absorbed by other goroutines trying to establish a new connection. Now the lock is acquired after the connection has been created. The downside of this approach is that concurrent goroutine may be trying to open a connection to the same target. The loser in the race will then have to Close the connection and use the winner's instead. Signed-off-by: Paulo Gomes <paulo.gomes@weave.works>
1 parent b264a35 commit 68eece4

File tree

1 file changed

+33
-24
lines changed

1 file changed

+33
-24
lines changed

pkg/git/libgit2/managed/ssh.go

+33-24
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ type sshSmartSubtransport struct {
102102
stdout io.Reader
103103
currentStream *sshSmartSubtransportStream
104104
ckey string
105+
addr string
105106
}
106107

107108
// aMux is the read-write mutex to control access to sshClients.
@@ -182,6 +183,7 @@ func (t *sshSmartSubtransport) Action(urlString string, action git2go.SmartServi
182183
port = u.Port()
183184
}
184185
addr = fmt.Sprintf("%s:%s", u.Hostname(), port)
186+
t.addr = addr
185187

186188
ckey, sshConfig, err := cacheKeyAndConfig(addr, cred)
187189
if err != nil {
@@ -229,9 +231,9 @@ func (t *sshSmartSubtransport) Action(urlString string, action git2go.SmartServi
229231
if t.session, err = t.client.NewSession(); err != nil {
230232
discardCachedSshClient(ckey)
231233

232-
// if the current connection was cached, and the error is EOF,
233-
// we can try again as this may be a stale connection.
234-
if !(cacheHit && err.Error() == "EOF") {
234+
// if the current connection was cached, we can try again
235+
// as this may be a stale connection.
236+
if !cacheHit {
235237
return nil, err
236238
}
237239

@@ -274,18 +276,16 @@ func (t *sshSmartSubtransport) Action(urlString string, action git2go.SmartServi
274276
}
275277

276278
func (t *sshSmartSubtransport) createConn(ckey, addr string, sshConfig *ssh.ClientConfig) error {
277-
aMux.Lock()
278-
defer aMux.Unlock()
279-
280279
// In some scenarios the ssh handshake can hang indefinitely at
281280
// golang.org/x/crypto/ssh.(*handshakeTransport).kexLoop.
282281
//
283282
// xref: https://github.com/golang/go/issues/51926
284283
done := make(chan error, 1)
285284
var err error
286285

286+
var c *ssh.Client
287287
go func() {
288-
t.client, err = ssh.Dial("tcp", addr, sshConfig)
288+
c, err = ssh.Dial("tcp", addr, sshConfig)
289289
done <- err
290290
}()
291291

@@ -304,8 +304,24 @@ func (t *sshSmartSubtransport) createConn(ckey, addr string, sshConfig *ssh.Clie
304304
return err
305305
}
306306

307+
t.client = c
308+
309+
// Mutex is set here to avoid the network latency being
310+
// absorbed by all competing goroutines.
311+
aMux.Lock()
312+
defer aMux.Unlock()
313+
314+
// A different goroutine won the race, dispose the connection
315+
// and carry on.
316+
if _, ok := sshClients[ckey]; ok {
317+
go func() {
318+
_ = c.Close()
319+
}()
320+
return nil
321+
}
322+
307323
sshClients[ckey] = &cachedClient{
308-
Client: t.client,
324+
Client: c,
309325
activeSessions: 1,
310326
}
311327

@@ -322,21 +338,16 @@ func (t *sshSmartSubtransport) createConn(ckey, addr string, sshConfig *ssh.Clie
322338
// may impair the transport to have successful actions on a new
323339
// SmartSubTransport (i.e. unreleased resources, staled connections).
324340
func (t *sshSmartSubtransport) Close() error {
325-
traceLog.Info("[ssh]: sshSmartSubtransport.Close()")
341+
traceLog.Info("[ssh]: sshSmartSubtransport.Close()", "server", t.addr)
326342
t.currentStream = nil
327343
if t.client != nil && t.stdin != nil {
328344
_ = t.stdin.Close()
329345
}
330346
t.client = nil
331347

332348
if t.session != nil {
333-
traceLog.Info("[ssh]: session.Close()")
334-
err := t.session.Close()
335-
// failure closing a session suggests a stale connection.
336-
if err != nil && t.ckey != "" {
337-
discardCachedSshClient(t.ckey)
338-
t.ckey = ""
339-
}
349+
traceLog.Info("[ssh]: session.Close()", "server", t.addr)
350+
_ = t.session.Close()
340351
}
341352
t.session = nil
342353

@@ -439,16 +450,14 @@ func discardCachedSshClient(key string) {
439450
defer aMux.Unlock()
440451

441452
if v, found := sshClients[key]; found {
442-
traceLog.Info("[ssh]: discard cached ssh client")
443-
444-
v.activeSessions--
453+
traceLog.Info("[ssh]: discard cached ssh client", "activeSessions", v.activeSessions)
445454
closeConn := func() {
446-
if v.Client != nil {
447-
// run as async goroutine to minimise mutex time in immediate closures.
448-
go func() {
455+
// run as async goroutine to minimise mutex time in immediate closures.
456+
go func() {
457+
if v.Client != nil {
449458
_ = v.Client.Close()
450-
}()
451-
}
459+
}
460+
}()
452461
}
453462

454463
// if no active sessions for this connection, close it right-away.

0 commit comments

Comments
 (0)