Skip to content

Commit 8462aef

Browse files
authored
fix(ttx): close endorsement sessions on all return paths (LFDT-Panurus#1893)
1 parent 0984e3c commit 8462aef

2 files changed

Lines changed: 91 additions & 17 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
// This white-box (package ttx) file covers cleanupSessions, which needs access to
8+
// the unexported sessions field. It is kept separate from the black-box
9+
// collectendorsements_test.go so the latter can keep importing dep/mock, which
10+
// cannot be imported from package ttx without creating an import cycle.
11+
package ttx
12+
13+
import (
14+
"context"
15+
"testing"
16+
17+
"github.com/hyperledger-labs/fabric-smart-client/platform/view/view"
18+
"github.com/stretchr/testify/assert"
19+
)
20+
21+
// countingSession is a minimal view.Session fake that records how many times
22+
// Close was called.
23+
type countingSession struct {
24+
closes int
25+
}
26+
27+
func (s *countingSession) Info() view.SessionInfo { return view.SessionInfo{} }
28+
func (s *countingSession) Send([]byte) error { return nil }
29+
func (s *countingSession) SendWithContext(context.Context, []byte) error { return nil }
30+
func (s *countingSession) SendError([]byte) error { return nil }
31+
func (s *countingSession) SendErrorWithContext(context.Context, []byte) error { return nil }
32+
func (s *countingSession) Receive() <-chan *view.Message { return nil }
33+
func (s *countingSession) Close() { s.closes++ }
34+
35+
// TestCleanupSessions_ClosesAllAndEmptiesMap verifies that cleanupSessions closes
36+
// every tracked session and clears the session map, so no session is leaked on any
37+
// return path of Call.
38+
func TestCleanupSessions_ClosesAllAndEmptiesMap(t *testing.T) {
39+
s1, s2 := &countingSession{}, &countingSession{}
40+
c := &CollectEndorsementsView{
41+
sessions: map[string]view.Session{"auditor": s1, "party": s2},
42+
}
43+
44+
c.cleanupSessions(t.Context())
45+
46+
assert.Equal(t, 1, s1.closes, "auditor session should be closed exactly once")
47+
assert.Equal(t, 1, s2.closes, "party session should be closed exactly once")
48+
assert.Empty(t, c.sessions, "session map should be emptied after cleanup")
49+
}
50+
51+
// TestCleanupSessions_Idempotent verifies that a second cleanupSessions call is a
52+
// no-op and does not close any session twice. This matters because the deferred
53+
// cleanup may run after sessions were already released earlier in the flow.
54+
func TestCleanupSessions_Idempotent(t *testing.T) {
55+
s := &countingSession{}
56+
c := &CollectEndorsementsView{
57+
sessions: map[string]view.Session{"auditor": s},
58+
}
59+
60+
c.cleanupSessions(t.Context())
61+
c.cleanupSessions(t.Context())
62+
63+
assert.Equal(t, 1, s.closes, "session must not be closed more than once across repeated cleanup calls")
64+
}
65+
66+
// TestCleanupSessions_NilAndEmptySafe verifies cleanupSessions tolerates an empty
67+
// map and nil session entries without panicking.
68+
func TestCleanupSessions_NilAndEmptySafe(t *testing.T) {
69+
// Empty map.
70+
empty := &CollectEndorsementsView{sessions: map[string]view.Session{}}
71+
assert.NotPanics(t, func() { empty.cleanupSessions(t.Context()) })
72+
73+
// Nil session entry.
74+
withNil := &CollectEndorsementsView{sessions: map[string]view.Session{"nil": nil}}
75+
assert.NotPanics(t, func() { withNil.cleanupSessions(t.Context()) })
76+
assert.Empty(t, withNil.sessions, "nil entries should also be removed")
77+
}

token/services/ttx/collectendorsements.go

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ func (c *CollectEndorsementsView) Call(context view.Context) (any, error) {
101101
// Ensure Done() is called on all external wallets regardless of errors
102102
defer c.CleanupExternalWallets(context, externalWallets)
103103

104+
// Close all endorsement sessions on every return path. Leaking the auditor
105+
// session on an error path keeps its audit-DB enrollment-ID lock held.
106+
defer c.cleanupSessions(context.Context())
107+
104108
// 1. First collect signatures on the token request
105109
issueSigmas, err := c.requestSignaturesOnIssues(context, externalWallets)
106110
if err != nil {
@@ -142,14 +146,7 @@ func (c *CollectEndorsementsView) Call(context view.Context) (any, error) {
142146
return nil, errors.WithMessagef(err, "failed distributing tx")
143147
}
144148

145-
// Cleanup audit
146-
logger.DebugfContext(context.Context(), "Cleanup audit")
147-
if err := c.cleanupAudit(context); err != nil {
148-
logger.ErrorfContext(context.Context(), "failed cleaning up audit: %s", err)
149-
150-
return nil, errors.WithMessagef(err, "failed cleaning up audit")
151-
}
152-
149+
// Sessions are closed by the deferred cleanupSessions call.
153150
logger.DebugfContext(context.Context(), "CollectEndorsementsView done.")
154151

155152
labels := []string{
@@ -451,18 +448,18 @@ func (c *CollectEndorsementsView) requestAudit(context view.Context) ([]view.Ide
451448
return nil, nil
452449
}
453450

454-
// cleanupAudit closes the auditor session if one was opened during the audit process.
455-
// This should be called after the transaction has been fully endorsed and distributed.
456-
func (c *CollectEndorsementsView) cleanupAudit(context view.Context) error {
457-
if !c.tx.Opts.Auditor.IsNone() {
458-
session, err := c.getSession(context, c.tx.Opts.Auditor)
459-
if err != nil {
460-
return errors.Wrap(err, "failed getting auditor's session")
451+
// cleanupSessions closes and removes every session opened while collecting
452+
// endorsements (e.g. the auditor session). Deleting entries as they are closed
453+
// makes it idempotent, so the deferred call is safe to run on any return path.
454+
func (c *CollectEndorsementsView) cleanupSessions(ctx context.Context) {
455+
for key, session := range c.sessions {
456+
delete(c.sessions, key)
457+
if session == nil {
458+
continue
461459
}
460+
logger.DebugfContext(ctx, "closing endorsement session [%s]", key)
462461
session.Close()
463462
}
464-
465-
return nil
466463
}
467464

468465
// distributeTxToParties distributes the endorsed transaction to all parties in the distribution list.

0 commit comments

Comments
 (0)