Skip to content

Commit afde10c

Browse files
Merge pull request #67 from kaleido-io/tx-index
Add transaction index and timestamps to events
2 parents 4e9a1a6 + e96ad59 commit afde10c

25 files changed

+1263
-431
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/google/certificate-transparency-go v1.1.1 // indirect
1212
github.com/google/uuid v1.3.0 // indirect
1313
github.com/gorilla/websocket v1.4.2
14+
github.com/hashicorp/golang-lru v0.5.4
1415
github.com/hyperledger/fabric-config v0.0.7 // indirect
1516
github.com/hyperledger/fabric-protos-go v0.0.0-20201028172056-a3136dde2354
1617
github.com/hyperledger/fabric-sdk-go v1.0.1-0.20210729165856-3be4ed253dcf

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
334334
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
335335
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
336336
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
337+
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
338+
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
337339
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
338340
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
339341
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=

internal/events/api/event.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
EventPayloadType_Bytes = "bytes" // default data type of the event payload, no special processing is done before returning to the subscribing client
2323
EventPayloadType_String = "string" // event payload will be an UTF-8 encoded string
2424
EventPayloadType_StringifiedJSON = "stringifiedJSON" // event payload will be a structured map with UTF-8 encoded string values
25+
EventPayloadType_JSON = "json" // equivalent to "stringifiedJSON"
2526
)
2627

2728
// persistedFilter is the part of the filter we record to storage
@@ -52,7 +53,7 @@ type SubscriptionInfo struct {
5253
Signer string `json:"signer"`
5354
FromBlock string `json:"fromBlock,omitempty"`
5455
Filter persistedFilter `json:"filter"`
55-
PayloadType string `json:"payloadType,omitempty"` // optional. data type of the payload bytes; "bytes", "string" or "stringifiedJSON". Default to "bytes"
56+
PayloadType string `json:"payloadType,omitempty"` // optional. data type of the payload bytes; "bytes", "string" or "stringifiedJSON/json". Default to "bytes"
5657
}
5758

5859
// GetID returns the ID (for sorting)
@@ -61,11 +62,12 @@ func (info *SubscriptionInfo) GetID() string {
6162
}
6263

6364
type EventEntry struct {
64-
ChaincodeId string `json:"chaincodeId"`
65-
BlockNumber uint64 `json:"blockNumber"`
66-
TransactionId string `json:"transactionId"`
67-
EventName string `json:"eventName"`
68-
Payload interface{} `json:"payload"`
69-
Timestamp uint64 `json:"timestamp,omitempty"`
70-
SubID string `json:"subId"`
65+
ChaincodeId string `json:"chaincodeId"`
66+
BlockNumber uint64 `json:"blockNumber"`
67+
TransactionId string `json:"transactionId"`
68+
TransactionIndex int `json:"transactionIndex"`
69+
EventName string `json:"eventName"`
70+
Payload interface{} `json:"payload"`
71+
Timestamp int64 `json:"timestamp,omitempty"`
72+
SubID string `json:"subId"`
7173
}

internal/events/eventstream.go

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
eventsapi "github.com/hyperledger/firefly-fabconnect/internal/events/api"
3131
"github.com/hyperledger/firefly-fabconnect/internal/ws"
3232

33+
lru "github.com/hashicorp/golang-lru"
3334
log "github.com/sirupsen/logrus"
3435
)
3536

@@ -73,6 +74,7 @@ type StreamInfo struct {
7374
Webhook *webhookActionInfo `json:"webhook,omitempty"`
7475
WebSocket *webSocketActionInfo `json:"websocket,omitempty"`
7576
Timestamps bool `json:"timestamps,omitempty"` // Include block timestamps in the events generated
77+
TimestampCacheSize int `json:"timestampCacheSize,omitempty"`
7678
}
7779

7880
type webhookActionInfo struct {
@@ -91,26 +93,27 @@ type webSocketActionInfo struct {
9193
type eventHandler func(*eventData)
9294

9395
type eventStream struct {
94-
sm subscriptionManager
95-
allowPrivateIPs bool
96-
spec *StreamInfo
97-
eventStream chan *eventData
98-
eventHandler eventHandler
99-
stopped bool
100-
processorDone bool
101-
pollingInterval time.Duration
102-
pollerDone bool
103-
inFlight uint64
104-
batchCond *sync.Cond
105-
batchQueue *list.List
106-
batchCount uint64
107-
initialRetryDelay time.Duration
108-
backoffFactor float64
109-
updateInProgress bool
110-
updateInterrupt chan struct{} // a zero-sized struct used only for signaling (hand rolled alternative to context)
111-
updateWG *sync.WaitGroup // Wait group for the go routines to reply back after they have stopped
112-
action eventStreamAction
113-
wsChannels ws.WebSocketChannels
96+
sm subscriptionManager
97+
allowPrivateIPs bool
98+
spec *StreamInfo
99+
eventStream chan *eventData
100+
eventHandler eventHandler
101+
stopped bool
102+
processorDone bool
103+
pollingInterval time.Duration
104+
pollerDone bool
105+
inFlight uint64
106+
batchCond *sync.Cond
107+
batchQueue *list.List
108+
batchCount uint64
109+
initialRetryDelay time.Duration
110+
backoffFactor float64
111+
updateInProgress bool
112+
updateInterrupt chan struct{} // a zero-sized struct used only for signaling (hand rolled alternative to context)
113+
updateWG *sync.WaitGroup // Wait group for the go routines to reply back after they have stopped
114+
action eventStreamAction
115+
wsChannels ws.WebSocketChannels
116+
blockTimestampCache *lru.Cache
114117
}
115118

116119
type eventStreamAction interface {
@@ -149,6 +152,9 @@ func newEventStream(sm subscriptionManager, spec *StreamInfo, wsChannels ws.WebS
149152
} else {
150153
spec.ErrorHandling = ErrorHandlingSkip
151154
}
155+
if spec.TimestampCacheSize == 0 {
156+
spec.TimestampCacheSize = DefaultTimestampCacheSize
157+
}
152158

153159
a = &eventStream{
154160
sm: sm,
@@ -164,6 +170,10 @@ func newEventStream(sm subscriptionManager, spec *StreamInfo, wsChannels ws.WebS
164170
}
165171
a.eventHandler = a.handleEvent
166172

173+
if a.blockTimestampCache, err = lru.New(spec.TimestampCacheSize); err != nil {
174+
return nil, errors.Errorf(errors.EventStreamsCreateStreamResourceErr, err)
175+
}
176+
167177
if a.pollingInterval == 0 {
168178
// Let's us do this from UTs, without exposing it
169179
a.pollingInterval = 10 * time.Millisecond

internal/events/eventstream_test.go

Lines changed: 20 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,12 @@
1717
package events
1818

1919
import (
20-
"encoding/json"
2120
"fmt"
22-
"net/http"
23-
"net/http/httptest"
2421
"strings"
2522
"sync"
2623
"testing"
2724
"time"
2825

29-
"github.com/hyperledger/fabric-protos-go/common"
30-
"github.com/hyperledger/fabric-protos-go/peer"
31-
"github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab"
32-
eventmocks "github.com/hyperledger/fabric-sdk-go/pkg/fab/events/service/mocks"
3326
"github.com/hyperledger/firefly-fabconnect/internal/conf"
3427
"github.com/hyperledger/firefly-fabconnect/internal/errors"
3528
eventsapi "github.com/hyperledger/firefly-fabconnect/internal/events/api"
@@ -40,119 +33,6 @@ import (
4033
"github.com/stretchr/testify/mock"
4134
)
4235

43-
func newTestStreamForBatching(spec *StreamInfo, db kvstore.KVStore, status ...int) (*subscriptionMGR, *eventStream, *httptest.Server, chan []*eventsapi.EventEntry) {
44-
mux := http.NewServeMux()
45-
eventStream := make(chan []*eventsapi.EventEntry)
46-
count := 0
47-
mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
48-
var events []*eventsapi.EventEntry
49-
_ = json.NewDecoder(req.Body).Decode(&events)
50-
eventStream <- events
51-
idx := count
52-
if idx >= len(status) {
53-
idx = len(status) - 1
54-
}
55-
res.WriteHeader(status[idx])
56-
count++
57-
})
58-
svr := httptest.NewServer(mux)
59-
if spec.Type == "" {
60-
spec.Type = "webhook"
61-
spec.Webhook.URL = svr.URL
62-
spec.Webhook.Headers = map[string]string{"x-my-header": "my-value"}
63-
}
64-
sm := newTestSubscriptionManager()
65-
sm.config.WebhooksAllowPrivateIPs = true
66-
sm.config.PollingIntervalSec = 0
67-
if db != nil {
68-
sm.db = db
69-
}
70-
mockstore, ok := sm.db.(*mockkvstore.KVStore)
71-
if ok {
72-
mockstore.On("Get", mock.Anything).Return([]byte(""), nil)
73-
mockstore.On("Put", mock.Anything, mock.Anything).Return(nil)
74-
}
75-
76-
_ = sm.addStream(spec)
77-
return sm, sm.streams[spec.ID], svr, eventStream
78-
}
79-
80-
func newTestStreamForWebSocket(spec *StreamInfo, db kvstore.KVStore, status ...int) (*subscriptionMGR, *eventStream, *mockWebSocket) {
81-
sm := newTestSubscriptionManager()
82-
sm.config.PollingIntervalSec = 0
83-
if db != nil {
84-
sm.db = db
85-
}
86-
_ = sm.addStream(spec)
87-
return sm, sm.streams[spec.ID], sm.wsChannels.(*mockWebSocket)
88-
}
89-
90-
func testEvent(subID string) *eventData {
91-
entry := &eventsapi.EventEntry{
92-
SubID: subID,
93-
}
94-
return &eventData{
95-
event: entry,
96-
batchComplete: func(*eventsapi.EventEntry) {},
97-
}
98-
}
99-
100-
func mockRPCClient(fromBlock string, withReset ...bool) *mockfabric.RPCClient {
101-
rpc := &mockfabric.RPCClient{}
102-
blockEventChan := make(chan *fab.BlockEvent)
103-
ccEventChan := make(chan *fab.CCEvent)
104-
var roBlockEventChan <-chan *fab.BlockEvent = blockEventChan
105-
var roCCEventChan <-chan *fab.CCEvent = ccEventChan
106-
res := &fab.BlockchainInfoResponse{
107-
BCI: &common.BlockchainInfo{
108-
Height: 10,
109-
},
110-
}
111-
rpc.On("SubscribeEvent", mock.Anything, mock.Anything).Return(nil, roBlockEventChan, roCCEventChan, nil)
112-
rpc.On("QueryChainInfo", mock.Anything, mock.Anything).Return(res, nil)
113-
rpc.On("Unregister", mock.Anything).Return()
114-
115-
go func() {
116-
if fromBlock == "0" {
117-
blockEventChan <- &fab.BlockEvent{
118-
Block: constructBlock(1),
119-
}
120-
}
121-
blockEventChan <- &fab.BlockEvent{
122-
Block: constructBlock(11),
123-
}
124-
if len(withReset) > 0 {
125-
blockEventChan <- &fab.BlockEvent{
126-
Block: constructBlock(11),
127-
}
128-
}
129-
}()
130-
131-
return rpc
132-
}
133-
134-
func setupTestSubscription(sm *subscriptionMGR, stream *eventStream, subscriptionName, fromBlock string, withReset ...bool) *eventsapi.SubscriptionInfo {
135-
rpc := mockRPCClient(fromBlock, withReset...)
136-
sm.rpc = rpc
137-
spec := &eventsapi.SubscriptionInfo{
138-
Name: subscriptionName,
139-
Stream: stream.spec.ID,
140-
}
141-
if fromBlock != "" {
142-
spec.FromBlock = fromBlock
143-
}
144-
_ = sm.addSubscription(spec)
145-
146-
return spec
147-
}
148-
149-
func constructBlock(number uint64) *common.Block {
150-
mockTx := eventmocks.NewTransactionWithCCEvent("testTxID", peer.TxValidationCode_VALID, "testChaincodeID", "testCCEventName", []byte("testPayload"))
151-
mockBlock := eventmocks.NewBlock("testChannelID", mockTx)
152-
mockBlock.Header.Number = number
153-
return mockBlock
154-
}
155-
15636
func TestConstructorNoSpec(t *testing.T) {
15737
assert := assert.New(t)
15838
_, err := newEventStream(newTestSubscriptionManager(), nil, nil)
@@ -500,7 +380,7 @@ func TestProcessEventsEnd2EndWebhook(t *testing.T) {
500380
&StreamInfo{
501381
BatchSize: 1,
502382
Webhook: &webhookActionInfo{},
503-
Timestamps: false,
383+
Timestamps: true,
504384
}, db, 200)
505385
defer svr.Close()
506386

@@ -512,9 +392,15 @@ func TestProcessEventsEnd2EndWebhook(t *testing.T) {
512392
wg := &sync.WaitGroup{}
513393
wg.Add(1)
514394
go func() {
395+
// the block event
515396
e1s := <-eventStream
516397
assert.Equal(1, len(e1s))
517398
assert.Equal(uint64(11), e1s[0].BlockNumber)
399+
// the chaincode event
400+
e2s := <-eventStream
401+
assert.Equal(1, len(e2s))
402+
assert.Equal(uint64(10), e2s[0].BlockNumber)
403+
assert.Equal(int64(1000000), e2s[0].Timestamp)
518404
wg.Done()
519405
}()
520406
wg.Wait()
@@ -537,7 +423,7 @@ func TestProcessEventsEnd2EndCatchupWebhook(t *testing.T) {
537423
_ = db.Init()
538424
sm, stream, svr, eventStream := newTestStreamForBatching(
539425
&StreamInfo{
540-
BatchSize: 1,
426+
BatchSize: 2,
541427
Webhook: &webhookActionInfo{},
542428
Timestamps: false,
543429
}, db, 200)
@@ -552,11 +438,9 @@ func TestProcessEventsEnd2EndCatchupWebhook(t *testing.T) {
552438
wg.Add(1)
553439
go func() {
554440
e1s := <-eventStream
555-
assert.Equal(1, len(e1s))
441+
assert.Equal(2, len(e1s))
556442
assert.Equal(uint64(1), e1s[0].BlockNumber)
557-
e2s := <-eventStream
558-
assert.Equal(1, len(e2s))
559-
assert.Equal(uint64(11), e2s[0].BlockNumber)
443+
assert.Equal(uint64(11), e1s[1].BlockNumber)
560444
wg.Done()
561445
}()
562446
wg.Wait()
@@ -636,6 +520,10 @@ func TestProcessEventsEnd2EndWithReset(t *testing.T) {
636520
e1s := <-eventStream
637521
assert.Equal(1, len(e1s))
638522
assert.Equal(uint64(11), e1s[0].BlockNumber)
523+
// the chaincode event
524+
e2s := <-eventStream
525+
assert.Equal(1, len(e2s))
526+
assert.Equal(uint64(10), e2s[0].BlockNumber)
639527
wg.Done()
640528
}()
641529
wg.Wait()
@@ -745,7 +633,7 @@ func TestPauseResumeAfterCheckpoint(t *testing.T) {
745633
wg := &sync.WaitGroup{}
746634
wg.Add(1)
747635
go func() {
748-
for i := 0; i < 1; i++ {
636+
for i := 0; i < 2; i++ {
749637
<-eventStream
750638
}
751639
wg.Done()
@@ -811,7 +699,7 @@ func TestPauseResumeBeforeCheckpoint(t *testing.T) {
811699
wg := &sync.WaitGroup{}
812700
wg.Add(1)
813701
go func() {
814-
for i := 0; i < 1; i++ {
702+
for i := 0; i < 2; i++ {
815703
<-eventStream
816704
}
817705
wg.Done()
@@ -851,7 +739,7 @@ func TestMarkStaleOnError(t *testing.T) {
851739
wg := &sync.WaitGroup{}
852740
wg.Add(1)
853741
go func() {
854-
for i := 0; i < 1; i++ {
742+
for i := 0; i < 2; i++ {
855743
<-eventStream
856744
}
857745
wg.Done()
@@ -929,7 +817,7 @@ func TestStoreCheckpointStoreError(t *testing.T) {
929817
wg := &sync.WaitGroup{}
930818
wg.Add(1)
931819
go func() {
932-
for i := 0; i < 1; i++ {
820+
for i := 0; i < 2; i++ {
933821
<-eventStream
934822
}
935823
wg.Done()
@@ -1199,6 +1087,7 @@ func TestUpdateStreamMissingWebhookURL(t *testing.T) {
11991087
wg := &sync.WaitGroup{}
12001088
wg.Add(1)
12011089
go func() {
1090+
<-eventStream
12021091
<-eventStream
12031092
wg.Done()
12041093
}()
@@ -1242,6 +1131,7 @@ func TestUpdateStreamInvalidWebhookURL(t *testing.T) {
12421131
wg := &sync.WaitGroup{}
12431132
wg.Add(1)
12441133
go func() {
1134+
<-eventStream
12451135
<-eventStream
12461136
wg.Done()
12471137
}()

internal/events/evtprocessor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (ep *evtProcessor) processEventEntry(subInfo *api.SubscriptionInfo, entry *
7373
switch payloadType {
7474
case api.EventPayloadType_String:
7575
entry.Payload = string(entry.Payload.([]byte))
76-
case api.EventPayloadType_StringifiedJSON:
76+
case api.EventPayloadType_StringifiedJSON, api.EventPayloadType_JSON:
7777
structuredMap := make(map[string]interface{})
7878
err := json.Unmarshal(entry.Payload.([]byte), &structuredMap)
7979
if err != nil {

0 commit comments

Comments
 (0)