Skip to content
This repository was archived by the owner on Apr 25, 2025. It is now read-only.

Commit cdd0b4d

Browse files
committed
[FABG-746] Allow transfer of event registrations
A new function was added to the EventClient API that allows you to transfer event registrations from an existing client and import them into a new client. The new client will start listening to blocks from where the old client left off. This allows you to update the configuration of an SDK without losing events. Change-Id: I5f8035b2c29a0a4e10a4f85c8ffa09d4153e04ec Signed-off-by: Bob Stasyszyn <Bob.Stasyszyn@securekey.com>
1 parent c5ec8d9 commit cdd0b4d

File tree

16 files changed

+1021
-71
lines changed

16 files changed

+1021
-71
lines changed

pkg/common/providers/fab/eventservice.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,29 @@ type ConnectionEvent struct {
113113
Err error
114114
}
115115

116+
// EventSnapshot contains a snapshot of the event client before it was stopped.
117+
// The snapshot includes all of the event registrations and the last block received.
118+
type EventSnapshot interface {
119+
// LastBlockReceived returns the block number of the last block received at the time
120+
// that the snapshot was taken.
121+
LastBlockReceived() uint64
122+
123+
// BlockRegistrations returns the block registrations.
124+
BlockRegistrations() []Registration
125+
126+
// FilteredBlockRegistrations returns the filtered block registrations.
127+
FilteredBlockRegistrations() []Registration
128+
129+
// CCRegistrations returns the chaincode registrations.
130+
CCRegistrations() []Registration
131+
132+
// TxStatusRegistrations returns the transaction status registrations.
133+
TxStatusRegistrations() []Registration
134+
135+
// Closes all registrations
136+
Close()
137+
}
138+
116139
// EventClient is a client that connects to a peer and receives channel events
117140
// such as block, filtered block, chaincode, and transaction status events.
118141
type EventClient interface {
@@ -131,4 +154,9 @@ type EventClient interface {
131154
// A return value of false indicates that the client could not be closed since
132155
// there was at least one registration.
133156
CloseIfIdle() bool
157+
158+
// TransferRegistrations transfers all registrations into an EventSnapshot.
159+
// The registrations are not closed and may be transferred to a new event client.
160+
// - close: If true then the client will also be closed
161+
TransferRegistrations(close bool) (EventSnapshot, error)
134162
}

pkg/fab/events/client/client.go

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -108,40 +108,66 @@ func (c *Client) Connect() error {
108108
// A return value of false indicates that the client could not be closed since
109109
// there was at least one registration.
110110
func (c *Client) CloseIfIdle() bool {
111-
return c.close(false)
111+
logger.Debug("Attempting to close event client...")
112+
113+
// Check if there are any outstanding registrations
114+
regInfoCh := make(chan *esdispatcher.RegistrationInfo)
115+
err := c.Submit(esdispatcher.NewRegistrationInfoEvent(regInfoCh))
116+
if err != nil {
117+
logger.Debugf("Submit failed %s", err)
118+
return false
119+
}
120+
regInfo := <-regInfoCh
121+
122+
logger.Debugf("Outstanding registrations: %d", regInfo.TotalRegistrations)
123+
124+
if regInfo.TotalRegistrations > 0 {
125+
logger.Debugf("Cannot stop client since there are %d outstanding registrations", regInfo.TotalRegistrations)
126+
return false
127+
}
128+
129+
c.Close()
130+
131+
return true
112132
}
113133

114134
// Close closes the connection to the event server and releases all resources.
115135
// Once this function is invoked the client may no longer be used.
116136
func (c *Client) Close() {
117-
c.close(true)
137+
c.close(func() {
138+
c.Stop()
139+
})
118140
}
119141

120-
func (c *Client) close(force bool) bool {
121-
logger.Debug("Attempting to close event client...")
142+
// TransferRegistrations transfers all registrations into an EventSnapshot.
143+
// The registrations are not closed and may susequently be transferred to a
144+
// new event client.
145+
// - close - if true then the client will also be closed
146+
func (c *Client) TransferRegistrations(close bool) (fab.EventSnapshot, error) {
147+
if !close {
148+
return c.Transfer()
149+
}
122150

123-
if !force {
124-
// Check if there are any outstanding registrations
125-
regInfoCh := make(chan *esdispatcher.RegistrationInfo)
126-
err := c.Submit(esdispatcher.NewRegistrationInfoEvent(regInfoCh))
151+
var snapshot fab.EventSnapshot
152+
var err error
153+
c.close(func() {
154+
logger.Debug("Stopping dispatcher and taking snapshot of all registrations...")
155+
snapshot, err = c.StopAndTransfer()
127156
if err != nil {
128-
logger.Debugf("Submit failed %s", err)
129-
return false
157+
logger.Errorf("An error occurred while stopping dispatcher and taking snapshot: %s", err)
130158
}
131-
regInfo := <-regInfoCh
159+
})
132160

133-
logger.Debugf("Outstanding registrations: %d", regInfo.TotalRegistrations)
161+
return snapshot, err
162+
}
134163

135-
if regInfo.TotalRegistrations > 0 {
136-
logger.Debugf("Cannot stop client since there are %d outstanding registrations", regInfo.TotalRegistrations)
137-
return false
138-
}
139-
}
164+
func (c *Client) close(stopHandler func()) {
165+
logger.Debug("Attempting to close event client...")
140166

141167
if !c.setStoppped() {
142168
// Already stopped
143169
logger.Debug("Client already stopped")
144-
return true
170+
return
145171
}
146172

147173
logger.Debug("Stopping client...")
@@ -154,7 +180,7 @@ func (c *Client) close(force bool) bool {
154180
err1 := c.Submit(dispatcher.NewDisconnectEvent(errch))
155181
if err1 != nil {
156182
logger.Debugf("Submit failed %s", err1)
157-
return false
183+
return
158184
}
159185
err := <-errch
160186

@@ -166,13 +192,11 @@ func (c *Client) close(force bool) bool {
166192

167193
logger.Debug("Stopping dispatcher...")
168194

169-
c.Stop()
195+
stopHandler()
170196

171197
c.mustSetConnectionState(Disconnected)
172198

173199
logger.Debug("... event client is stopped")
174-
175-
return true
176200
}
177201

178202
func (c *Client) connect() error {

pkg/fab/events/client/client_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
"testing"
1616
"time"
1717

18+
"github.com/stretchr/testify/require"
19+
1820
"github.com/hyperledger/fabric-sdk-go/pkg/common/options"
1921
"github.com/hyperledger/fabric-sdk-go/pkg/common/providers/context"
2022
"github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab"
@@ -1457,3 +1459,86 @@ func TestDisconnectIfBlockHeightLags(t *testing.T) {
14571459
t.Fatal("Timed out waiting for reconnect")
14581460
}
14591461
}
1462+
1463+
func TestTransferRegistrations(t *testing.T) {
1464+
// Tests the scenario where all event registrations are transferred to another event client.
1465+
t.Run("Transfer", func(t *testing.T) {
1466+
testTransferRegistrations(t, func(client *Client) (fab.EventSnapshot, error) {
1467+
return client.TransferRegistrations(false)
1468+
})
1469+
})
1470+
1471+
// Tests the scenario where one event client is stopped and all
1472+
// of the event registrations are transferred to another event client.
1473+
t.Run("TransferAndClose", func(t *testing.T) {
1474+
testTransferRegistrations(t, func(client *Client) (fab.EventSnapshot, error) {
1475+
return client.TransferRegistrations(true)
1476+
})
1477+
})
1478+
}
1479+
1480+
type transferFunc func(client *Client) (fab.EventSnapshot, error)
1481+
1482+
// TestTransferRegistrations tests the scenario where one event client is stopped and all
1483+
// of the event registrations are transferred to another event client.
1484+
func testTransferRegistrations(t *testing.T, transferFunc transferFunc) {
1485+
channelID := "mychannel"
1486+
eventClient1, conn1, err := newClientWithMockConn(
1487+
fabmocks.NewMockContext(
1488+
mspmocks.NewMockSigningIdentity("user1", "Org1MSP"),
1489+
),
1490+
fabmocks.NewMockChannelCfg(channelID),
1491+
clientmocks.NewDiscoveryService(peer1, peer2),
1492+
clientProvider,
1493+
mockconn.WithLedger(servicemocks.NewMockLedger(servicemocks.BlockEventFactory, sourceURL)),
1494+
)
1495+
require.NoErrorf(t, err, "error creating channel event client")
1496+
1497+
err = eventClient1.Connect()
1498+
require.NoErrorf(t, err, "error connecting channel event client")
1499+
1500+
breg, beventch, err := eventClient1.RegisterBlockEvent()
1501+
require.NoErrorf(t, err, "error registering for block events")
1502+
1503+
conn1.Ledger().NewBlock(channelID,
1504+
servicemocks.NewTransaction("txID", pb.TxValidationCode_VALID, cb.HeaderType_ENDORSER_TRANSACTION),
1505+
)
1506+
1507+
select {
1508+
case <-beventch:
1509+
case <-time.After(5 * time.Second):
1510+
t.Fatal("timed out waiting for block event")
1511+
}
1512+
1513+
snapshot, err := transferFunc(eventClient1)
1514+
require.NoErrorf(t, err, "error transferring snapshot")
1515+
1516+
eventClient2, conn2, err := newClientWithMockConnAndOpts(
1517+
fabmocks.NewMockContext(
1518+
mspmocks.NewMockSigningIdentity("user1", "Org1MSP"),
1519+
),
1520+
fabmocks.NewMockChannelCfg("mychannel"),
1521+
clientmocks.NewDiscoveryService(peer1, peer2),
1522+
nil, clientProvider,
1523+
[]options.Opt{
1524+
esdispatcher.WithSnapshot(snapshot),
1525+
},
1526+
mockconn.WithLedger(servicemocks.NewMockLedger(servicemocks.BlockEventFactory, sourceURL)),
1527+
)
1528+
require.NoErrorf(t, err, "error creating channel event client")
1529+
1530+
err = eventClient2.Connect()
1531+
require.NoErrorf(t, err, "error connecting channel event client")
1532+
1533+
conn2.Ledger().NewBlock(channelID,
1534+
servicemocks.NewTransaction("txID", pb.TxValidationCode_VALID, cb.HeaderType_ENDORSER_TRANSACTION),
1535+
)
1536+
1537+
select {
1538+
case <-beventch:
1539+
case <-time.After(5 * time.Second):
1540+
t.Fatal("timed out waiting for block event")
1541+
}
1542+
1543+
eventClient2.Unregister(breg)
1544+
}

pkg/fab/events/client/dispatcher/dispatcher.go

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package dispatcher
88

99
import (
1010
"fmt"
11+
"math"
1112
"sync"
1213
"time"
1314

@@ -238,22 +239,61 @@ func (ed *Dispatcher) clearConnectionRegistration() {
238239
}
239240

240241
func (ed *Dispatcher) filterByBlockHeght(peers []fab.Peer) []fab.Peer {
241-
if ed.blockHeightLagThreshold < 0 || len(peers) == 1 {
242-
logger.Debugf("Returning all peers")
243-
return peers
242+
var minBlockHeight uint64
243+
if ed.minBlockHeight > 0 {
244+
if ed.LastBlockNum() != math.MaxUint64 {
245+
// No blocks received yet
246+
logger.Debugf("Min block height was specified: %d", ed.minBlockHeight)
247+
minBlockHeight = ed.minBlockHeight
248+
} else {
249+
// Make sure minBlockHeight is greater than the last block received
250+
if ed.minBlockHeight > ed.LastBlockNum() {
251+
minBlockHeight = ed.minBlockHeight
252+
} else {
253+
minBlockHeight = ed.LastBlockNum() + 1
254+
logger.Debugf("Min block height was specified as %d but the last block received was %d. Setting min height to %d", ed.minBlockHeight, ed.LastBlockNum(), minBlockHeight)
255+
}
256+
}
244257
}
245258

246-
maxHeight := getMaxBlockHeight(peers)
247-
logger.Debugf("Max block height of peers: %d", maxHeight)
248-
249-
if maxHeight <= uint64(ed.blockHeightLagThreshold) {
250-
logger.Debugf("Max block height of peers is %d and lag threshold is %d so returning all peers", maxHeight, ed.blockHeightLagThreshold)
251-
return peers
259+
retPeers := ed.doFilterByBlockHeght(minBlockHeight, peers)
260+
if len(retPeers) == 0 && minBlockHeight > 0 {
261+
// The last block that was received may have been the last block in the channel. Try again with lastBlock-1.
262+
logger.Infof("No peers at the minimum height %d. Trying again with min height %d ...", minBlockHeight, minBlockHeight-1)
263+
minBlockHeight--
264+
retPeers = ed.doFilterByBlockHeght(minBlockHeight, peers)
265+
if len(retPeers) == 0 {
266+
// No peers at the given height. Try again without min height
267+
logger.Infof("No peers at the minimum height %d. Trying again without min height ...", minBlockHeight)
268+
retPeers = ed.doFilterByBlockHeght(0, peers)
269+
}
252270
}
253271

254-
cutoffHeight := maxHeight - uint64(ed.blockHeightLagThreshold)
272+
return retPeers
273+
}
274+
275+
func (ed *Dispatcher) doFilterByBlockHeght(minBlockHeight uint64, peers []fab.Peer) []fab.Peer {
276+
var cutoffHeight uint64
277+
if minBlockHeight > 0 {
278+
logger.Debugf("Setting cutoff height to be min block height: %d ...", minBlockHeight)
279+
cutoffHeight = minBlockHeight
280+
} else {
281+
if ed.blockHeightLagThreshold < 0 || len(peers) == 1 {
282+
logger.Debugf("Returning all peers")
283+
return peers
284+
}
285+
286+
maxHeight := getMaxBlockHeight(peers)
287+
logger.Debugf("Max block height of peers: %d", maxHeight)
288+
289+
if maxHeight <= uint64(ed.blockHeightLagThreshold) {
290+
logger.Debugf("Max block height of peers is %d and lag threshold is %d so returning all peers", maxHeight, ed.blockHeightLagThreshold)
291+
return peers
292+
}
293+
cutoffHeight = maxHeight - uint64(ed.blockHeightLagThreshold)
294+
}
255295

256-
logger.Debugf("Choosing peers whose block heights are greater than the cutoff height %d ...", cutoffHeight)
296+
logger.Debugf("Choosing peers whose block heights are at least the cutoff height %d ...", cutoffHeight)
257297

258298
var retPeers []fab.Peer
259299
for _, p := range peers {

pkg/fab/events/client/dispatcher/dispatcher_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ func TestConnectionEvent(t *testing.T) {
213213
t.Fatalf("Error stopping dispatcher: %s", err1)
214214
}
215215

216+
// Wait for event that test is done
216217
err = <-errch
217218
if err != nil {
218219
t.Fatal(err.Error())

pkg/fab/events/client/dispatcher/opts.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
type params struct {
1818
loadBalancePolicy lbp.LoadBalancePolicy
19+
minBlockHeight uint64
1920
blockHeightMonitorPeriod time.Duration
2021
blockHeightLagThreshold int
2122
reconnectBlockHeightLagThreshold int
@@ -110,3 +111,14 @@ func (p *params) SetBlockHeightMonitorPeriod(value time.Duration) {
110111
logger.Debugf("BlockHeightMonitorPeriod: %s", value)
111112
p.blockHeightMonitorPeriod = value
112113
}
114+
115+
func (p *params) SetFromBlock(value uint64) {
116+
logger.Debugf("FromBlock: %d", value)
117+
p.minBlockHeight = value + 1
118+
}
119+
120+
func (p *params) SetSnapshot(value fab.EventSnapshot) error {
121+
logger.Debugf("SetSnapshot.FromBlock: %d", value)
122+
p.minBlockHeight = value.LastBlockReceived() + 1
123+
return nil
124+
}

pkg/fab/events/deliverclient/deliverclient.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ var logger = logging.NewLogger("fabsdk/fab")
2828

2929
// deliverProvider is the connection provider used for connecting to the Deliver service
3030
var deliverProvider = func(context fabcontext.Client, chConfig fab.ChannelCfg, peer fab.Peer) (api.Connection, error) {
31+
if peer == nil {
32+
return nil, errors.New("Peer is nil")
33+
}
34+
3135
eventEndpoint, ok := peer.(api.EventEndpoint)
3236
if !ok {
3337
panic("peer is not an EventEndpoint")
@@ -37,6 +41,10 @@ var deliverProvider = func(context fabcontext.Client, chConfig fab.ChannelCfg, p
3741

3842
// deliverFilteredProvider is the connection provider used for connecting to the DeliverFiltered service
3943
var deliverFilteredProvider = func(context fabcontext.Client, chConfig fab.ChannelCfg, peer fab.Peer) (api.Connection, error) {
44+
if peer == nil {
45+
return nil, errors.New("Peer is nil")
46+
}
47+
4048
eventEndpoint, ok := peer.(api.EventEndpoint)
4149
if !ok {
4250
panic("peer is not an EventEndpoint")

0 commit comments

Comments
 (0)