Skip to content

Commit 2de9ac6

Browse files
authored
V2 preview (#66)
2 parents d695598 + 93912bb commit 2de9ac6

55 files changed

Lines changed: 2782 additions & 1100 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

go/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ type Config struct {
175175
WsURL string // Websocket Api url
176176

177177
WsHA bool // Use concurrent connections to multiple Streams servers
178+
WsAllowOutOfOrder bool // Allow out-of-order reports through while still deduplicating HA duplicates
178179
WsMaxReconnect int // Maximum number of reconnection attempts for Stream underlying connections
179180
LogDebug bool // Log debug information
180181
InsecureSkipVerify bool // Skip server certificate chain and host name verification
@@ -292,6 +293,7 @@ func (r *ReportResponse) UnmarshalJSON(b []byte) (err error)
292293
type Stats struct {
293294
Accepted uint64 // Total number of accepted reports
294295
Deduplicated uint64 // Total number of deduplicated reports when in HA
296+
OutOfOrder uint64 // Total number of out-of-order reports seen
295297
TotalReceived uint64 // Total number of received reports
296298
PartialReconnects uint64 // Total number of partial reconnects when in HA
297299
FullReconnects uint64 // Total number of full reconnects

go/client.go

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"strings"
1616
"time"
1717

18-
"github.com/smartcontractkit/data-streams-sdk/go/feed"
18+
"github.com/smartcontractkit/data-streams-sdk/go/v2/feed"
1919
)
2020

2121
// Client is the data streams client interface.
@@ -151,25 +151,41 @@ func (c *client) GetLatestReport(ctx context.Context, id feed.ID) (r *ReportResp
151151
// ReportResponse implements the report envelope that contains the full report payload,
152152
// its FeedID and timestamps. For decoding the Report Payload use report.Decode().
153153
type ReportResponse struct {
154-
FeedID feed.ID `json:"feedID"`
155-
FullReport []byte `json:"fullReport"`
156-
ValidFromTimestamp uint64 `json:"validFromTimestamp"`
157-
ObservationsTimestamp uint64 `json:"observationsTimestamp"`
154+
FeedID feed.ID
155+
FullReport []byte
156+
ValidFromTimestamp time.Time
157+
ObservationsTimestamp time.Time
158158
}
159159

160160
func (r *ReportResponse) UnmarshalJSON(b []byte) (err error) {
161-
type Alias ReportResponse
162161
aux := &struct {
163-
FullReport string `json:"fullReport"`
164-
*Alias
165-
}{
166-
Alias: (*Alias)(r),
167-
}
162+
FeedID feed.ID `json:"feedID"`
163+
FullReport string `json:"fullReport"`
164+
ValidFromTimestamp uint64 `json:"validFromTimestamp"`
165+
ObservationsTimestamp uint64 `json:"observationsTimestamp"`
166+
ValidFromTimestampMs uint64 `json:"validFromTimestampMs"`
167+
ObservationsTimestampMs uint64 `json:"observationsTimestampMs"`
168+
}{}
168169

169170
if err := json.Unmarshal(b, aux); err != nil {
170171
return err
171172
}
172173

174+
r.FeedID = aux.FeedID
175+
176+
// V2 payloads use milliseconds, V1 payloads use seconds
177+
if aux.ValidFromTimestampMs > 0 {
178+
r.ValidFromTimestamp = time.UnixMilli(int64(aux.ValidFromTimestampMs))
179+
} else if aux.ValidFromTimestamp > 0 {
180+
r.ValidFromTimestamp = time.Unix(int64(aux.ValidFromTimestamp), 0)
181+
}
182+
183+
if aux.ObservationsTimestampMs > 0 {
184+
r.ObservationsTimestamp = time.UnixMilli(int64(aux.ObservationsTimestampMs))
185+
} else if aux.ObservationsTimestamp > 0 {
186+
r.ObservationsTimestamp = time.Unix(int64(aux.ObservationsTimestamp), 0)
187+
}
188+
173189
if len(aux.FullReport) < 3 {
174190
return nil
175191
}
@@ -182,13 +198,26 @@ func (r *ReportResponse) UnmarshalJSON(b []byte) (err error) {
182198
}
183199

184200
func (r *ReportResponse) MarshalJSON() ([]byte, error) {
185-
type Alias ReportResponse
201+
var validFrom, observationsTS uint64
202+
203+
// Wrapper timestamps are always in milliseconds
204+
if !r.ValidFromTimestamp.IsZero() {
205+
validFrom = uint64(r.ValidFromTimestamp.UnixMilli())
206+
}
207+
if !r.ObservationsTimestamp.IsZero() {
208+
observationsTS = uint64(r.ObservationsTimestamp.UnixMilli())
209+
}
210+
186211
return json.Marshal(&struct {
187-
FullReport string `json:"fullReport"`
188-
*Alias
212+
FeedID feed.ID `json:"feedID"`
213+
FullReport string `json:"fullReport"`
214+
ValidFromTimestampMs uint64 `json:"validFromTimestampMs"`
215+
ObservationsTimestampMs uint64 `json:"observationsTimestampMs"`
189216
}{
190-
FullReport: "0x" + hex.EncodeToString(r.FullReport),
191-
Alias: (*Alias)(r),
217+
FeedID: r.FeedID,
218+
FullReport: "0x" + hex.EncodeToString(r.FullReport),
219+
ValidFromTimestampMs: validFrom,
220+
ObservationsTimestampMs: observationsTS,
192221
})
193222
}
194223

@@ -243,7 +272,7 @@ func (c *client) GetReportPage(ctx context.Context, id feed.ID, pageTS uint64) (
243272
}
244273
r.NextPageTS = 0
245274
if len(r.Reports) > 0 {
246-
r.NextPageTS = r.Reports[len(r.Reports)-1].ObservationsTimestamp + 1
275+
r.NextPageTS = uint64(r.Reports[len(r.Reports)-1].ObservationsTimestamp.Unix()) + 1
247276
}
248277
return r, err
249278
}

go/client_test.go

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package streams
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"fmt"
@@ -10,11 +11,53 @@ import (
1011
"strconv"
1112
"strings"
1213
"testing"
14+
"time"
1315

1416
"github.com/ethereum/go-ethereum/common/hexutil"
15-
"github.com/smartcontractkit/data-streams-sdk/go/feed"
17+
"github.com/smartcontractkit/data-streams-sdk/go/v2/feed"
1618
)
1719

20+
// reportResponseEqual compares two ReportResponse structs, handling time.Time comparison properly
21+
func reportResponseEqual(a, b *ReportResponse) bool {
22+
if a.FeedID != b.FeedID {
23+
return false
24+
}
25+
if !bytes.Equal(a.FullReport, b.FullReport) {
26+
return false
27+
}
28+
if !a.ObservationsTimestamp.Equal(b.ObservationsTimestamp) {
29+
return false
30+
}
31+
if !a.ValidFromTimestamp.Equal(b.ValidFromTimestamp) {
32+
return false
33+
}
34+
return true
35+
}
36+
37+
// reportResponsesEqual compares slices of ReportResponse
38+
func reportResponsesEqual(a, b []*ReportResponse) bool {
39+
if len(a) != len(b) {
40+
return false
41+
}
42+
for i := range a {
43+
if !reportResponseEqual(a[i], b[i]) {
44+
return false
45+
}
46+
}
47+
return true
48+
}
49+
50+
// reportPageEqual compares two ReportPage structs
51+
func reportPageEqual(a, b *ReportPage) bool {
52+
if !reportResponsesEqual(a.Reports, b.Reports) {
53+
return false
54+
}
55+
if a.NextPageTS != b.NextPageTS {
56+
return false
57+
}
58+
return true
59+
}
60+
1861
func mustFeedIDfromString(s string) (f feed.ID) {
1962
err := f.FromString(s)
2063
if err != nil {
@@ -68,8 +111,8 @@ func TestClient_GetFeeds(t *testing.T) {
68111

69112
func TestClient_GetReports(t *testing.T) {
70113
expectedReports := []*ReportResponse{
71-
{FeedID: feed1, ObservationsTimestamp: 12344},
72-
{FeedID: feed2, ObservationsTimestamp: 12344},
114+
{FeedID: feed1, ObservationsTimestamp: time.Unix(12344, 0)},
115+
{FeedID: feed2, ObservationsTimestamp: time.Unix(12344, 0)},
73116
}
74117
expectedFeedIdListStr := fmt.Sprintf("%s,%s", feed1.String(), feed2.String())
75118

@@ -111,7 +154,7 @@ func TestClient_GetReports(t *testing.T) {
111154

112155
fmt.Println(expectedReports[0], reports[0])
113156

114-
if !reflect.DeepEqual(reports, expectedReports) {
157+
if !reportResponsesEqual(reports, expectedReports) {
115158
t.Errorf("GetFeeds() = %v, want %v", reports, expectedReports)
116159
}
117160
}
@@ -155,7 +198,7 @@ func TestClient_GetLatestReport(t *testing.T) {
155198
t.Fatalf("GetLatestReport() error = %v", err)
156199
}
157200

158-
if !reflect.DeepEqual(report, expectedReport) {
201+
if !reportResponseEqual(report, expectedReport) {
159202
t.Errorf("GetLatestReport() = %v, want %v", report, expectedReport)
160203
}
161204
}
@@ -165,18 +208,18 @@ func TestClient_GetReportPage(t *testing.T) {
165208

166209
expectedReportPage1 := &ReportPage{
167210
Reports: []*ReportResponse{
168-
{FeedID: feed1, ObservationsTimestamp: 1234567890, FullReport: hexutil.Bytes(`report1 payload`)},
169-
{FeedID: feed1, ObservationsTimestamp: 1234567891, FullReport: hexutil.Bytes(`report2 payload`)},
211+
{FeedID: feed1, FullReport: hexutil.Bytes(`report1 payload`), ObservationsTimestamp: time.Unix(1234567897, 0)},
212+
{FeedID: feed1, FullReport: hexutil.Bytes(`report2 payload`), ObservationsTimestamp: time.Unix(1234567898, 0)},
170213
},
171-
NextPageTS: 1234567892,
214+
NextPageTS: 1234567899, // Last ObservationsTimestamp (1234567898) + 1
172215
}
173216

174217
expectedReportPage2 := &ReportPage{
175218
Reports: []*ReportResponse{
176-
{FeedID: feed1, ObservationsTimestamp: 1234567892, FullReport: hexutil.Bytes(`report3 payload`)},
177-
{FeedID: feed1, ObservationsTimestamp: 1234567893, FullReport: hexutil.Bytes(`report4 payload`)},
219+
{FeedID: feed1, FullReport: hexutil.Bytes(`report3 payload`), ObservationsTimestamp: time.Unix(1234567997, 0)},
220+
{FeedID: feed1, FullReport: hexutil.Bytes(`report4 payload`), ObservationsTimestamp: time.Unix(1234567998, 0)},
178221
},
179-
NextPageTS: 1234567894,
222+
NextPageTS: 1234567999, // Last ObservationsTimestamp (1234567998) + 1
180223
}
181224

182225
ms := newMockServer(func(w http.ResponseWriter, r *http.Request) {
@@ -230,7 +273,7 @@ func TestClient_GetReportPage(t *testing.T) {
230273
t.Fatalf("GetReportPage() error = %v", err)
231274
}
232275

233-
if !reflect.DeepEqual(reportPage, expectedReportPage1) {
276+
if !reportPageEqual(reportPage, expectedReportPage1) {
234277
t.Errorf("GetReportPage() = %v, want %v", reportPage, expectedReportPage1)
235278
}
236279

@@ -239,7 +282,7 @@ func TestClient_GetReportPage(t *testing.T) {
239282
t.Fatalf("GetReportPage() error = %v", err)
240283
}
241284

242-
if !reflect.DeepEqual(reportPage, expectedReportPage2) {
285+
if !reportPageEqual(reportPage, expectedReportPage2) {
243286
t.Errorf("GetReportPage() = %v, want %v", reportPage, expectedReportPage2)
244287
}
245288
}

go/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type Config struct {
1515
WsURL string // Websocket Api url
1616
wsURL *url.URL // Websocket Api url
1717
WsHA bool // Use concurrent connections to multiple Streams servers
18+
WsAllowOutOfOrder bool // Allow out-of-order reports through while still deduplicating HA duplicates
1819
WsMaxReconnect int // Maximum number of reconnection attempts for Stream underlying connections
1920
LogDebug bool // Log debug information
2021
InsecureSkipVerify bool // Skip server certificate chain and host name verification

go/dedup.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package streams
2+
3+
import "sync"
4+
5+
const seenBufferSize = 32
6+
7+
type Verdict int
8+
9+
const (
10+
Accept Verdict = iota
11+
Duplicate
12+
OutOfOrder
13+
)
14+
15+
type feedState struct {
16+
watermark int64
17+
ring [seenBufferSize]int64
18+
set map[int64]struct{}
19+
cursor int
20+
count int
21+
}
22+
23+
type FeedDeduplicator struct {
24+
mu sync.Mutex
25+
feeds map[string]*feedState
26+
}
27+
28+
func NewFeedDeduplicator() *FeedDeduplicator {
29+
return &FeedDeduplicator{feeds: make(map[string]*feedState)}
30+
}
31+
32+
func (d *FeedDeduplicator) Check(feedID string, ts int64) Verdict {
33+
d.mu.Lock()
34+
defer d.mu.Unlock()
35+
36+
fs := d.feeds[feedID]
37+
if fs == nil {
38+
fs = &feedState{set: make(map[int64]struct{}, seenBufferSize)}
39+
d.feeds[feedID] = fs
40+
}
41+
42+
if _, dup := fs.set[ts]; dup {
43+
return Duplicate
44+
}
45+
46+
if fs.count == seenBufferSize {
47+
evict := fs.ring[fs.cursor]
48+
delete(fs.set, evict)
49+
} else {
50+
fs.count++
51+
}
52+
fs.ring[fs.cursor] = ts
53+
fs.set[ts] = struct{}{}
54+
fs.cursor = (fs.cursor + 1) % seenBufferSize
55+
56+
isOutOfOrder := fs.watermark > 0 && ts < fs.watermark
57+
if isOutOfOrder {
58+
return OutOfOrder
59+
}
60+
61+
if ts > fs.watermark {
62+
fs.watermark = ts
63+
}
64+
65+
return Accept
66+
}

0 commit comments

Comments
 (0)