feedx 0.12.7 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +2 -37
- data/.golangci.yml +13 -4
- data/.rubocop.yml +8 -14
- data/.tool-versions +1 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +54 -68
- data/Makefile +3 -3
- data/README.md +3 -1
- data/compression.go +29 -0
- data/compression_test.go +73 -61
- data/consumer.go +96 -152
- data/consumer_test.go +124 -59
- data/example_test.go +140 -0
- data/feedx.gemspec +2 -10
- data/feedx.go +16 -31
- data/feedx_ext_test.go +13 -3
- data/feedx_test.go +24 -26
- data/format.go +29 -19
- data/format_test.go +84 -56
- data/go.mod +11 -7
- data/go.sum +16 -138
- data/incremental.go +122 -0
- data/incremental_test.go +62 -0
- data/lib/feedx/cache/abstract.rb +3 -3
- data/lib/feedx/cache/value.rb +6 -6
- data/lib/feedx/compression/abstract.rb +2 -2
- data/lib/feedx/compression/gzip.rb +4 -4
- data/lib/feedx/consumer.rb +8 -8
- data/lib/feedx/format/abstract.rb +6 -6
- data/lib/feedx/format/json.rb +2 -2
- data/lib/feedx/format/protobuf.rb +6 -6
- data/lib/feedx/format.rb +1 -3
- data/lib/feedx/producer.rb +11 -11
- data/lib/feedx/stream.rb +2 -2
- data/lib/feedx.rb +2 -3
- data/manifest.go +65 -0
- data/producer.go +34 -137
- data/producer_test.go +46 -60
- data/reader.go +142 -41
- data/reader_test.go +86 -35
- data/scheduler.go +176 -0
- data/scheduler_test.go +128 -0
- data/writer.go +13 -13
- data/writer_test.go +61 -44
- metadata +12 -137
- data/.github/workflows/lint.yml +0 -18
- data/ext/parquet/decoder.go +0 -59
- data/ext/parquet/decoder_test.go +0 -88
- data/ext/parquet/encoder.go +0 -27
- data/ext/parquet/encoder_test.go +0 -70
- data/ext/parquet/go.mod +0 -12
- data/ext/parquet/go.sum +0 -193
- data/ext/parquet/parquet.go +0 -78
- data/ext/parquet/parquet_test.go +0 -28
- data/ext/parquet/testdata/alltypes_plain.parquet +0 -0
- data/lib/feedx/format/parquet.rb +0 -102
- data/spec/feedx/cache/memory_spec.rb +0 -23
- data/spec/feedx/cache/value_spec.rb +0 -19
- data/spec/feedx/compression/gzip_spec.rb +0 -17
- data/spec/feedx/compression/none_spec.rb +0 -15
- data/spec/feedx/compression_spec.rb +0 -19
- data/spec/feedx/consumer_spec.rb +0 -49
- data/spec/feedx/format/abstract_spec.rb +0 -21
- data/spec/feedx/format/json_spec.rb +0 -27
- data/spec/feedx/format/parquet_spec.rb +0 -30
- data/spec/feedx/format/protobuf_spec.rb +0 -23
- data/spec/feedx/format_spec.rb +0 -21
- data/spec/feedx/producer_spec.rb +0 -74
- data/spec/feedx/stream_spec.rb +0 -109
- data/spec/spec_helper.rb +0 -57
data/consumer.go
CHANGED
|
@@ -2,213 +2,157 @@ package feedx
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"errors"
|
|
5
6
|
"sync/atomic"
|
|
6
|
-
"time"
|
|
7
7
|
|
|
8
8
|
"github.com/bsm/bfs"
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
type
|
|
13
|
-
ReaderOptions
|
|
14
|
-
|
|
15
|
-
// The interval used by consumer to check the remote changes.
|
|
16
|
-
// Default: 1m
|
|
17
|
-
Interval time.Duration
|
|
18
|
-
|
|
19
|
-
// AfterSync callbacks are triggered after each sync, receiving
|
|
20
|
-
// the sync state and error (if occurred).
|
|
21
|
-
AfterSync func(*ConsumerSync, error)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
func (o *ConsumerOptions) norm(name string) {
|
|
25
|
-
o.ReaderOptions.norm(name)
|
|
26
|
-
if o.Interval <= 0 {
|
|
27
|
-
o.Interval = time.Minute
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ConsumerSync contains the state of the last sync.
|
|
32
|
-
type ConsumerSync struct {
|
|
33
|
-
// Consumer exposes the current consumer state.
|
|
34
|
-
Consumer
|
|
35
|
-
// Updated indicates is the sync resulted in an update.
|
|
36
|
-
Updated bool
|
|
37
|
-
// PreviousData references the data before the update.
|
|
38
|
-
// It allows to apply finalizers to data structures created by ConsumeFunc.
|
|
39
|
-
// This is only set when an update happened.
|
|
40
|
-
PreviousData interface{}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ConsumeFunc is a parsing callback which is run by the consumer every sync interval.
|
|
44
|
-
type ConsumeFunc func(*Reader) (data interface{}, err error)
|
|
11
|
+
// ConsumeFunc is a callback invoked by consumers.
|
|
12
|
+
type ConsumeFunc func(context.Context, *Reader) error
|
|
45
13
|
|
|
46
14
|
// Consumer manages data retrieval from a remote feed.
|
|
47
15
|
// It queries the feed in regular intervals, continuously retrieving new updates.
|
|
48
16
|
type Consumer interface {
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
LastModified() time.Time
|
|
57
|
-
// NumRead returns the number of values consumed during the last sync.
|
|
58
|
-
NumRead() int
|
|
17
|
+
// Consume initiates a sync attempt. It will consume the remote feed only if it has changed since
|
|
18
|
+
// last invocation.
|
|
19
|
+
Consume(context.Context, *ReaderOptions, ConsumeFunc) (*Status, error)
|
|
20
|
+
|
|
21
|
+
// Version indicates the most recently consumed version.
|
|
22
|
+
Version() int64
|
|
23
|
+
|
|
59
24
|
// Close stops the underlying sync process.
|
|
60
25
|
Close() error
|
|
61
26
|
}
|
|
62
27
|
|
|
63
28
|
// NewConsumer starts a new feed consumer.
|
|
64
|
-
func NewConsumer(ctx context.Context, remoteURL string
|
|
29
|
+
func NewConsumer(ctx context.Context, remoteURL string) (Consumer, error) {
|
|
65
30
|
remote, err := bfs.NewObject(ctx, remoteURL)
|
|
66
31
|
if err != nil {
|
|
67
32
|
return nil, err
|
|
68
33
|
}
|
|
69
34
|
|
|
70
|
-
csm
|
|
71
|
-
if err != nil {
|
|
72
|
-
_ = remote.Close()
|
|
73
|
-
return nil, err
|
|
74
|
-
}
|
|
35
|
+
csm := NewConsumerForRemote(remote)
|
|
75
36
|
csm.(*consumer).ownRemote = true
|
|
76
37
|
return csm, nil
|
|
77
38
|
}
|
|
78
39
|
|
|
79
40
|
// NewConsumerForRemote starts a new feed consumer with a remote.
|
|
80
|
-
func NewConsumerForRemote(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
o = *opt
|
|
84
|
-
}
|
|
85
|
-
o.norm(remote.Name())
|
|
86
|
-
|
|
87
|
-
ctx, stop := context.WithCancel(ctx)
|
|
88
|
-
c := &consumer{
|
|
89
|
-
remote: remote,
|
|
90
|
-
opt: o,
|
|
91
|
-
ctx: ctx,
|
|
92
|
-
stop: stop,
|
|
93
|
-
cfn: cfn,
|
|
94
|
-
}
|
|
41
|
+
func NewConsumerForRemote(remote *bfs.Object) Consumer {
|
|
42
|
+
return &consumer{remote: remote}
|
|
43
|
+
}
|
|
95
44
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
45
|
+
// NewIncrementalConsumer starts a new incremental feed consumer.
|
|
46
|
+
func NewIncrementalConsumer(ctx context.Context, bucketURL string) (Consumer, error) {
|
|
47
|
+
bucket, err := bfs.Connect(ctx, bucketURL)
|
|
48
|
+
if err != nil {
|
|
99
49
|
return nil, err
|
|
100
50
|
}
|
|
101
51
|
|
|
102
|
-
|
|
103
|
-
|
|
52
|
+
csm := NewIncrementalConsumerForBucket(bucket)
|
|
53
|
+
csm.(*consumer).ownBucket = true
|
|
54
|
+
return csm, nil
|
|
55
|
+
}
|
|
104
56
|
|
|
105
|
-
|
|
57
|
+
// NewIncrementalConsumerForBucket starts a new incremental feed consumer with a bucket.
|
|
58
|
+
func NewIncrementalConsumerForBucket(bucket bfs.Bucket) Consumer {
|
|
59
|
+
return &consumer{
|
|
60
|
+
remote: bfs.NewObjectFromBucket(bucket, "manifest.json"),
|
|
61
|
+
ownRemote: true,
|
|
62
|
+
bucket: bucket,
|
|
63
|
+
}
|
|
106
64
|
}
|
|
107
65
|
|
|
108
66
|
type consumer struct {
|
|
109
67
|
remote *bfs.Object
|
|
110
68
|
ownRemote bool
|
|
111
69
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
stop context.CancelFunc
|
|
115
|
-
|
|
116
|
-
cfn ConsumeFunc
|
|
117
|
-
data atomic.Value
|
|
118
|
-
|
|
119
|
-
numRead, lastMod, lastSync, lastConsumed int64
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Data implements Consumer interface.
|
|
123
|
-
func (c *consumer) Data() interface{} {
|
|
124
|
-
return c.data.Load()
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// NumRead implements Consumer interface.
|
|
128
|
-
func (c *consumer) NumRead() int {
|
|
129
|
-
return int(atomic.LoadInt64(&c.numRead))
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// LastSync implements Consumer interface.
|
|
133
|
-
func (c *consumer) LastSync() time.Time {
|
|
134
|
-
return timestamp(atomic.LoadInt64(&c.lastSync)).Time()
|
|
135
|
-
}
|
|
70
|
+
bucket bfs.Bucket
|
|
71
|
+
ownBucket bool
|
|
136
72
|
|
|
137
|
-
|
|
138
|
-
func (c *consumer) LastConsumed() time.Time {
|
|
139
|
-
return timestamp(atomic.LoadInt64(&c.lastConsumed)).Time()
|
|
73
|
+
version atomic.Int64
|
|
140
74
|
}
|
|
141
75
|
|
|
142
|
-
//
|
|
143
|
-
func (c *consumer)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Close implements Consumer interface.
|
|
148
|
-
func (c *consumer) Close() error {
|
|
149
|
-
c.stop()
|
|
150
|
-
if c.ownRemote {
|
|
151
|
-
return c.remote.Close()
|
|
76
|
+
// Consume implements Consumer interface.
|
|
77
|
+
func (c *consumer) Consume(ctx context.Context, opt *ReaderOptions, fn ConsumeFunc) (*Status, error) {
|
|
78
|
+
localVersion := c.Version()
|
|
79
|
+
status := Status{
|
|
80
|
+
LocalVersion: localVersion,
|
|
152
81
|
}
|
|
153
|
-
return nil
|
|
154
|
-
}
|
|
155
82
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
defer func() {
|
|
159
|
-
atomic.StoreInt64(&c.lastSync, syncTime)
|
|
160
|
-
}()
|
|
161
|
-
|
|
162
|
-
// retrieve original last modified time
|
|
163
|
-
lastMod, err := remoteLastModified(c.ctx, c.remote)
|
|
83
|
+
// retrieve remote mtime
|
|
84
|
+
remoteVersion, err := fetchRemoteVersion(ctx, c.remote)
|
|
164
85
|
if err != nil {
|
|
165
86
|
return nil, err
|
|
166
87
|
}
|
|
88
|
+
status.RemoteVersion = remoteVersion
|
|
167
89
|
|
|
168
|
-
// skip
|
|
169
|
-
if
|
|
170
|
-
|
|
90
|
+
// skip sync unless modified
|
|
91
|
+
if skipSync(remoteVersion, localVersion) {
|
|
92
|
+
status.Skipped = true
|
|
93
|
+
return &status, nil
|
|
171
94
|
}
|
|
172
95
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
96
|
+
var reader *Reader
|
|
97
|
+
if c.isIncremental() {
|
|
98
|
+
if reader, err = c.newIncrementalReader(ctx, opt); err != nil {
|
|
99
|
+
return nil, err
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
if reader, err = NewReader(ctx, c.remote, opt); err != nil {
|
|
103
|
+
return nil, err
|
|
104
|
+
}
|
|
177
105
|
}
|
|
178
106
|
defer reader.Close()
|
|
179
107
|
|
|
180
108
|
// consume feed
|
|
181
|
-
|
|
182
|
-
if err != nil {
|
|
109
|
+
if err := fn(ctx, reader); err != nil {
|
|
183
110
|
return nil, err
|
|
184
111
|
}
|
|
185
112
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
atomic.StoreInt64(&c.numRead, int64(reader.NumRead()))
|
|
190
|
-
atomic.StoreInt64(&c.lastMod, lastMod.Millis())
|
|
191
|
-
atomic.StoreInt64(&c.lastConsumed, syncTime)
|
|
192
|
-
return &ConsumerSync{
|
|
193
|
-
Consumer: c,
|
|
194
|
-
Updated: true,
|
|
195
|
-
PreviousData: previous,
|
|
196
|
-
}, nil
|
|
113
|
+
status.NumItems = reader.NumRead()
|
|
114
|
+
c.version.Store(remoteVersion)
|
|
115
|
+
return &status, nil
|
|
197
116
|
}
|
|
198
117
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
118
|
+
// Version implements Consumer interface.
|
|
119
|
+
func (c *consumer) Version() int64 {
|
|
120
|
+
return c.version.Load()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Close implements Consumer interface.
|
|
124
|
+
func (c *consumer) Close() (err error) {
|
|
125
|
+
if c.ownRemote && c.remote != nil {
|
|
126
|
+
if e := c.remote.Close(); e != nil {
|
|
127
|
+
err = errors.Join(err, e)
|
|
128
|
+
}
|
|
129
|
+
c.remote = nil
|
|
130
|
+
}
|
|
131
|
+
if c.ownBucket && c.bucket != nil {
|
|
132
|
+
if e := c.bucket.Close(); e != nil {
|
|
133
|
+
err = errors.Join(err, e)
|
|
212
134
|
}
|
|
135
|
+
c.bucket = nil
|
|
136
|
+
}
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func (c *consumer) isIncremental() bool {
|
|
141
|
+
return c.bucket != nil
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func (c *consumer) newIncrementalReader(ctx context.Context, opt *ReaderOptions) (*Reader, error) {
|
|
145
|
+
manifest, err := loadManifest(ctx, c.remote)
|
|
146
|
+
if err != nil {
|
|
147
|
+
return nil, err
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
files := manifest.Files
|
|
151
|
+
remotes := make([]*bfs.Object, 0, len(files))
|
|
152
|
+
for _, file := range files {
|
|
153
|
+
remotes = append(remotes, bfs.NewObjectFromBucket(c.bucket, file))
|
|
213
154
|
}
|
|
155
|
+
r := MultiReader(ctx, remotes, opt)
|
|
156
|
+
r.ownRemotes = true
|
|
157
|
+
return r, nil
|
|
214
158
|
}
|
data/consumer_test.go
CHANGED
|
@@ -2,83 +2,148 @@ package feedx_test
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"reflect"
|
|
6
|
+
"testing"
|
|
7
7
|
|
|
8
8
|
"github.com/bsm/bfs"
|
|
9
9
|
"github.com/bsm/feedx"
|
|
10
10
|
"github.com/bsm/feedx/internal/testdata"
|
|
11
|
-
. "github.com/bsm/ginkgo"
|
|
12
|
-
. "github.com/bsm/gomega"
|
|
13
11
|
)
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
for {
|
|
23
|
-
var msg testdata.MockMessage
|
|
24
|
-
if err := r.Decode(&msg); err == io.EOF {
|
|
25
|
-
break
|
|
26
|
-
} else if err != nil {
|
|
27
|
-
return nil, err
|
|
28
|
-
}
|
|
29
|
-
msgs = append(msgs, &msg)
|
|
13
|
+
func TestConsumer(t *testing.T) {
|
|
14
|
+
t.Run("consumes", func(t *testing.T) {
|
|
15
|
+
csm := fixConsumer(t, 101)
|
|
16
|
+
defer csm.Close()
|
|
17
|
+
|
|
18
|
+
if exp, got := int64(0), csm.Version(); exp != got {
|
|
19
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
30
20
|
}
|
|
31
|
-
return msgs, nil
|
|
32
|
-
}
|
|
33
21
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
// first attempt
|
|
23
|
+
msgs := testConsume(t, csm, &feedx.Status{
|
|
24
|
+
LocalVersion: 0,
|
|
25
|
+
RemoteVersion: 101,
|
|
26
|
+
Skipped: false,
|
|
27
|
+
NumItems: 2,
|
|
28
|
+
})
|
|
29
|
+
if exp, got := int64(101), csm.Version(); exp != got {
|
|
30
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
31
|
+
}
|
|
32
|
+
if exp, got := 2, len(msgs); exp != got {
|
|
33
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
34
|
+
}
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
// second attempt
|
|
37
|
+
_ = testConsume(t, csm, &feedx.Status{
|
|
38
|
+
LocalVersion: 101,
|
|
39
|
+
RemoteVersion: 101,
|
|
40
|
+
Skipped: true,
|
|
41
|
+
NumItems: 0,
|
|
42
|
+
})
|
|
41
43
|
})
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
t.Run("always if no version", func(t *testing.T) {
|
|
46
|
+
csm := fixConsumer(t, 0)
|
|
47
|
+
defer csm.Close()
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Expect(subject.LastConsumed()).To(BeTemporally("==", subject.LastSync()))
|
|
50
|
-
Expect(subject.LastModified()).To(BeTemporally("==", mockTime.Truncate(time.Millisecond)))
|
|
51
|
-
Expect(subject.NumRead()).To(Equal(2))
|
|
52
|
-
Expect(subject.Data()).To(ConsistOf(seed(), seed()))
|
|
53
|
-
Expect(subject.Close()).To(Succeed())
|
|
49
|
+
testConsume(t, csm, &feedx.Status{NumItems: 2})
|
|
50
|
+
testConsume(t, csm, &feedx.Status{NumItems: 2})
|
|
54
51
|
})
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
t.Run("incremental", func(t *testing.T) {
|
|
54
|
+
csm := fixIncrementalConsumer(t, 101)
|
|
55
|
+
defer csm.Close()
|
|
56
|
+
|
|
57
|
+
// first attempt
|
|
58
|
+
msgs := testConsume(t, csm, &feedx.Status{
|
|
59
|
+
LocalVersion: 0,
|
|
60
|
+
RemoteVersion: 101,
|
|
61
|
+
NumItems: 4,
|
|
62
|
+
})
|
|
63
|
+
if exp, got := int64(101), csm.Version(); exp != got {
|
|
64
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
65
|
+
}
|
|
66
|
+
if exp, got := 4, len(msgs); exp != got {
|
|
67
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
68
|
+
}
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
// second attempt
|
|
71
|
+
_ = testConsume(t, csm, &feedx.Status{
|
|
72
|
+
LocalVersion: 101,
|
|
73
|
+
RemoteVersion: 101,
|
|
74
|
+
Skipped: true,
|
|
75
|
+
})
|
|
66
76
|
})
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func fixConsumer(t *testing.T, version int64) feedx.Consumer {
|
|
81
|
+
t.Helper()
|
|
82
|
+
|
|
83
|
+
obj := bfs.NewInMemObject("path/to/file.json")
|
|
84
|
+
t.Cleanup(func() { _ = obj.Close() })
|
|
85
|
+
|
|
86
|
+
if err := writeN(obj, 2, version); err != nil {
|
|
87
|
+
t.Fatal("unexpected error", err)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
csm := feedx.NewConsumerForRemote(obj)
|
|
91
|
+
t.Cleanup(func() { _ = csm.Close() })
|
|
92
|
+
|
|
93
|
+
return csm
|
|
94
|
+
}
|
|
71
95
|
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
func fixIncrementalConsumer(t *testing.T, version int64) feedx.Consumer {
|
|
97
|
+
t.Helper()
|
|
74
98
|
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
bucket := bfs.NewInMem()
|
|
100
|
+
obj1 := bfs.NewObjectFromBucket(bucket, "data-0-0.json")
|
|
101
|
+
if err := writeN(obj1, 2, 0); err != nil {
|
|
102
|
+
t.Fatal("unexpected error", err)
|
|
103
|
+
}
|
|
104
|
+
defer obj1.Close()
|
|
105
|
+
|
|
106
|
+
obj2 := bfs.NewObjectFromBucket(bucket, "data-0-1.json")
|
|
107
|
+
if err := writeN(obj2, 2, 0); err != nil {
|
|
108
|
+
t.Fatal("unexpected error", err)
|
|
109
|
+
}
|
|
110
|
+
defer obj2.Close()
|
|
111
|
+
|
|
112
|
+
objm := bfs.NewObjectFromBucket(bucket, "manifest.json")
|
|
113
|
+
defer objm.Close()
|
|
114
|
+
|
|
115
|
+
manifest := &feedx.Manifest{
|
|
116
|
+
Version: version,
|
|
117
|
+
Files: []string{obj1.Name(), obj2.Name()},
|
|
118
|
+
}
|
|
119
|
+
writer := feedx.NewWriter(t.Context(), objm, &feedx.WriterOptions{Version: version})
|
|
120
|
+
defer writer.Discard()
|
|
121
|
+
|
|
122
|
+
if err := writer.Encode(manifest); err != nil {
|
|
123
|
+
t.Fatal("unexpected error", err)
|
|
124
|
+
} else if err := writer.Commit(); err != nil {
|
|
125
|
+
t.Fatal("unexpected error", err)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
csm := feedx.NewIncrementalConsumerForBucket(bucket)
|
|
129
|
+
t.Cleanup(func() { _ = csm.Close() })
|
|
130
|
+
|
|
131
|
+
return csm
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func testConsume(t *testing.T, csm feedx.Consumer, exp *feedx.Status) (msgs []*testdata.MockMessage) {
|
|
135
|
+
t.Helper()
|
|
77
136
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
Expect(csmr.LastConsumed()).To(BeTemporally("==", csmr.LastSync())) // consumed on last sync
|
|
82
|
-
Expect(csmr.LastModified()).To(BeTemporally("==", time.Unix(0, 0)))
|
|
137
|
+
status, err := csm.Consume(t.Context(), nil, func(ctx context.Context, r *feedx.Reader) (err error) {
|
|
138
|
+
msgs, err = readMessages(r)
|
|
139
|
+
return err
|
|
83
140
|
})
|
|
84
|
-
|
|
141
|
+
if err != nil {
|
|
142
|
+
t.Fatal("unexpected error", err)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if !reflect.DeepEqual(exp, status) {
|
|
146
|
+
t.Errorf("expected %#v, got %#v", exp, status)
|
|
147
|
+
}
|
|
148
|
+
return
|
|
149
|
+
}
|
data/example_test.go
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
package feedx_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"time"
|
|
9
|
+
|
|
10
|
+
"github.com/bsm/bfs"
|
|
11
|
+
"github.com/bsm/feedx"
|
|
12
|
+
"github.com/bsm/feedx/internal/testdata"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type message = testdata.MockMessage
|
|
16
|
+
|
|
17
|
+
func Example() {
|
|
18
|
+
ctx := context.TODO()
|
|
19
|
+
|
|
20
|
+
// create an mock object
|
|
21
|
+
obj := bfs.NewInMemObject("todos.ndjson")
|
|
22
|
+
defer obj.Close()
|
|
23
|
+
|
|
24
|
+
pcr := feedx.NewProducerForRemote(obj)
|
|
25
|
+
defer pcr.Close()
|
|
26
|
+
|
|
27
|
+
// produce
|
|
28
|
+
status, err := pcr.Produce(ctx, 101, nil, func(w *feedx.Writer) error {
|
|
29
|
+
return errors.Join(
|
|
30
|
+
w.Encode(&message{Name: "Jane", Height: 175}),
|
|
31
|
+
w.Encode(&message{Name: "Joe", Height: 172}),
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
if err != nil {
|
|
35
|
+
panic(err)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fmt.Printf("PRODUCED skipped:%v version:%v->%v items:%v\n", status.Skipped, status.LocalVersion, status.RemoteVersion, status.NumItems)
|
|
39
|
+
|
|
40
|
+
// create a consumer
|
|
41
|
+
csm := feedx.NewConsumerForRemote(obj)
|
|
42
|
+
defer csm.Close()
|
|
43
|
+
|
|
44
|
+
// consume data
|
|
45
|
+
var msgs []*message
|
|
46
|
+
status, err = csm.Consume(context.TODO(), nil, func(ctx context.Context, r *feedx.Reader) error {
|
|
47
|
+
for {
|
|
48
|
+
var msg message
|
|
49
|
+
if err := r.Decode(&msg); err != nil {
|
|
50
|
+
if errors.Is(err, io.EOF) {
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
return err
|
|
54
|
+
}
|
|
55
|
+
msgs = append(msgs, &msg)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return nil
|
|
59
|
+
})
|
|
60
|
+
if err != nil {
|
|
61
|
+
panic(err)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fmt.Printf("CONSUMED skipped:%v version:%v->%v items:%v\n", status.Skipped, status.LocalVersion, status.RemoteVersion, status.NumItems)
|
|
65
|
+
fmt.Printf("DATA [%q, %q]\n", msgs[0].Name, msgs[1].Name)
|
|
66
|
+
|
|
67
|
+
// Output:
|
|
68
|
+
// PRODUCED skipped:false version:101->0 items:2
|
|
69
|
+
// CONSUMED skipped:false version:0->101 items:2
|
|
70
|
+
// DATA ["Jane", "Joe"]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func ExampleScheduler_Consume() {
|
|
74
|
+
ctx := context.TODO()
|
|
75
|
+
|
|
76
|
+
// create an mock object
|
|
77
|
+
obj := bfs.NewInMemObject("todos.ndjson")
|
|
78
|
+
defer obj.Close()
|
|
79
|
+
|
|
80
|
+
// create a consumer
|
|
81
|
+
csm := feedx.NewConsumerForRemote(obj)
|
|
82
|
+
defer csm.Close()
|
|
83
|
+
|
|
84
|
+
job := feedx.Every(time.Hour).
|
|
85
|
+
WithContext(ctx).
|
|
86
|
+
BeforeSync(func(_ int64) bool {
|
|
87
|
+
fmt.Println("1. Before sync")
|
|
88
|
+
return true
|
|
89
|
+
}).
|
|
90
|
+
AfterSync(func(_ *feedx.Status, err error) {
|
|
91
|
+
fmt.Printf("3. After sync - error:%v", err)
|
|
92
|
+
}).
|
|
93
|
+
Consume(csm, func(_ context.Context, _ *feedx.Reader) error {
|
|
94
|
+
fmt.Println("2. Consuming feed")
|
|
95
|
+
return nil
|
|
96
|
+
})
|
|
97
|
+
job.Stop()
|
|
98
|
+
|
|
99
|
+
// Output:
|
|
100
|
+
// 1. Before sync
|
|
101
|
+
// 2. Consuming feed
|
|
102
|
+
// 3. After sync - error:<nil>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func ExampleScheduler_Produce() {
|
|
106
|
+
ctx := context.TODO()
|
|
107
|
+
|
|
108
|
+
// create an mock object
|
|
109
|
+
obj := bfs.NewInMemObject("todos.ndjson")
|
|
110
|
+
defer obj.Close()
|
|
111
|
+
|
|
112
|
+
// create a producer
|
|
113
|
+
pcr := feedx.NewProducerForRemote(obj)
|
|
114
|
+
defer pcr.Close()
|
|
115
|
+
|
|
116
|
+
job := feedx.Every(time.Hour).
|
|
117
|
+
WithContext(ctx).
|
|
118
|
+
BeforeSync(func(_ int64) bool {
|
|
119
|
+
fmt.Println("2. Before sync")
|
|
120
|
+
return true
|
|
121
|
+
}).
|
|
122
|
+
AfterSync(func(_ *feedx.Status, err error) {
|
|
123
|
+
fmt.Printf("4. After sync - error:%v", err)
|
|
124
|
+
}).
|
|
125
|
+
WithVersionCheck(func(_ context.Context) (int64, error) {
|
|
126
|
+
fmt.Println("1. Retrieve latest version")
|
|
127
|
+
return 101, nil
|
|
128
|
+
}).
|
|
129
|
+
Produce(pcr, func(w *feedx.Writer) error {
|
|
130
|
+
fmt.Println("3. Producing feed")
|
|
131
|
+
return nil
|
|
132
|
+
})
|
|
133
|
+
job.Stop()
|
|
134
|
+
|
|
135
|
+
// Output:
|
|
136
|
+
// 1. Retrieve latest version
|
|
137
|
+
// 2. Before sync
|
|
138
|
+
// 3. Producing feed
|
|
139
|
+
// 4. After sync - error:<nil>
|
|
140
|
+
}
|
data/feedx.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Gem::Specification.new do |s|
|
|
2
2
|
s.name = 'feedx'
|
|
3
|
-
s.version = '0.
|
|
3
|
+
s.version = '0.14.0'
|
|
4
4
|
s.authors = ['Black Square Media Ltd']
|
|
5
5
|
s.email = ['info@blacksquaremedia.com']
|
|
6
6
|
s.summary = %(Exchange data between components via feeds)
|
|
@@ -9,17 +9,9 @@ Gem::Specification.new do |s|
|
|
|
9
9
|
s.license = 'Apache-2.0'
|
|
10
10
|
|
|
11
11
|
s.files = `git ls-files -z`.split("\x0").reject {|f| f.start_with?('spec/') }
|
|
12
|
-
s.test_files = `git ls-files -z -- spec/*`.split("\x0")
|
|
13
12
|
s.require_paths = ['lib']
|
|
14
|
-
s.required_ruby_version = '>= 2
|
|
13
|
+
s.required_ruby_version = '>= 3.2'
|
|
15
14
|
|
|
16
15
|
s.add_dependency 'bfs', '>= 0.8.0'
|
|
17
|
-
|
|
18
|
-
s.add_development_dependency 'bundler'
|
|
19
|
-
s.add_development_dependency 'pbio'
|
|
20
|
-
s.add_development_dependency 'rake'
|
|
21
|
-
s.add_development_dependency 'red-parquet', '>= 3.0', '< 4.0'
|
|
22
|
-
s.add_development_dependency 'rspec'
|
|
23
|
-
s.add_development_dependency 'rubocop-bsm'
|
|
24
16
|
s.metadata['rubygems_mfa_required'] = 'true'
|
|
25
17
|
end
|