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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -37
  3. data/.golangci.yml +13 -4
  4. data/.rubocop.yml +8 -14
  5. data/.tool-versions +1 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +54 -68
  8. data/Makefile +3 -3
  9. data/README.md +3 -1
  10. data/compression.go +29 -0
  11. data/compression_test.go +73 -61
  12. data/consumer.go +96 -152
  13. data/consumer_test.go +124 -59
  14. data/example_test.go +140 -0
  15. data/feedx.gemspec +2 -10
  16. data/feedx.go +16 -31
  17. data/feedx_ext_test.go +13 -3
  18. data/feedx_test.go +24 -26
  19. data/format.go +29 -19
  20. data/format_test.go +84 -56
  21. data/go.mod +11 -7
  22. data/go.sum +16 -138
  23. data/incremental.go +122 -0
  24. data/incremental_test.go +62 -0
  25. data/lib/feedx/cache/abstract.rb +3 -3
  26. data/lib/feedx/cache/value.rb +6 -6
  27. data/lib/feedx/compression/abstract.rb +2 -2
  28. data/lib/feedx/compression/gzip.rb +4 -4
  29. data/lib/feedx/consumer.rb +8 -8
  30. data/lib/feedx/format/abstract.rb +6 -6
  31. data/lib/feedx/format/json.rb +2 -2
  32. data/lib/feedx/format/protobuf.rb +6 -6
  33. data/lib/feedx/format.rb +1 -3
  34. data/lib/feedx/producer.rb +11 -11
  35. data/lib/feedx/stream.rb +2 -2
  36. data/lib/feedx.rb +2 -3
  37. data/manifest.go +65 -0
  38. data/producer.go +34 -137
  39. data/producer_test.go +46 -60
  40. data/reader.go +142 -41
  41. data/reader_test.go +86 -35
  42. data/scheduler.go +176 -0
  43. data/scheduler_test.go +128 -0
  44. data/writer.go +13 -13
  45. data/writer_test.go +61 -44
  46. metadata +12 -137
  47. data/.github/workflows/lint.yml +0 -18
  48. data/ext/parquet/decoder.go +0 -59
  49. data/ext/parquet/decoder_test.go +0 -88
  50. data/ext/parquet/encoder.go +0 -27
  51. data/ext/parquet/encoder_test.go +0 -70
  52. data/ext/parquet/go.mod +0 -12
  53. data/ext/parquet/go.sum +0 -193
  54. data/ext/parquet/parquet.go +0 -78
  55. data/ext/parquet/parquet_test.go +0 -28
  56. data/ext/parquet/testdata/alltypes_plain.parquet +0 -0
  57. data/lib/feedx/format/parquet.rb +0 -102
  58. data/spec/feedx/cache/memory_spec.rb +0 -23
  59. data/spec/feedx/cache/value_spec.rb +0 -19
  60. data/spec/feedx/compression/gzip_spec.rb +0 -17
  61. data/spec/feedx/compression/none_spec.rb +0 -15
  62. data/spec/feedx/compression_spec.rb +0 -19
  63. data/spec/feedx/consumer_spec.rb +0 -49
  64. data/spec/feedx/format/abstract_spec.rb +0 -21
  65. data/spec/feedx/format/json_spec.rb +0 -27
  66. data/spec/feedx/format/parquet_spec.rb +0 -30
  67. data/spec/feedx/format/protobuf_spec.rb +0 -23
  68. data/spec/feedx/format_spec.rb +0 -21
  69. data/spec/feedx/producer_spec.rb +0 -74
  70. data/spec/feedx/stream_spec.rb +0 -109
  71. 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
- // ConsumerOptions configure the consumer instance.
12
- type ConsumerOptions struct {
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
- // Data returns the data as returned by ConsumeFunc on last sync.
50
- Data() interface{}
51
- // LastSync returns time of last sync attempt.
52
- LastSync() time.Time
53
- // LastConsumed returns time of last feed consumption.
54
- LastConsumed() time.Time
55
- // LastModified returns time at which the remote feed was last modified.
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, opt *ConsumerOptions, cfn ConsumeFunc) (Consumer, error) {
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, err := NewConsumerForRemote(ctx, remote, opt, cfn)
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(ctx context.Context, remote *bfs.Object, opt *ConsumerOptions, cfn ConsumeFunc) (Consumer, error) {
81
- var o ConsumerOptions
82
- if opt != nil {
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
- // run initial sync
97
- if _, err := c.sync(true); err != nil {
98
- _ = c.Close()
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
- // start continuous loop
103
- go c.loop()
52
+ csm := NewIncrementalConsumerForBucket(bucket)
53
+ csm.(*consumer).ownBucket = true
54
+ return csm, nil
55
+ }
104
56
 
105
- return c, nil
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
- opt ConsumerOptions
113
- ctx context.Context
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
- // LastConsumed implements Consumer interface.
138
- func (c *consumer) LastConsumed() time.Time {
139
- return timestamp(atomic.LoadInt64(&c.lastConsumed)).Time()
73
+ version atomic.Int64
140
74
  }
141
75
 
142
- // LastModified implements Consumer interface.
143
- func (c *consumer) LastModified() time.Time {
144
- return timestamp(atomic.LoadInt64(&c.lastMod)).Time()
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
- func (c *consumer) sync(force bool) (*ConsumerSync, error) {
157
- syncTime := timestampFromTime(time.Now()).Millis()
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 update if not forced or modified
169
- if !force && lastMod > 0 && lastMod.Millis() == atomic.LoadInt64(&c.lastMod) {
170
- return &ConsumerSync{Consumer: c}, nil
90
+ // skip sync unless modified
91
+ if skipSync(remoteVersion, localVersion) {
92
+ status.Skipped = true
93
+ return &status, nil
171
94
  }
172
95
 
173
- // open remote reader
174
- reader, err := NewReader(c.ctx, c.remote, &c.opt.ReaderOptions)
175
- if err != nil {
176
- return nil, err
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
- data, err := c.cfn(reader)
182
- if err != nil {
109
+ if err := fn(ctx, reader); err != nil {
183
110
  return nil, err
184
111
  }
185
112
 
186
- // update stores
187
- previous := c.data.Load()
188
- c.data.Store(data)
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
- func (c *consumer) loop() {
200
- ticker := time.NewTicker(c.opt.Interval)
201
- defer ticker.Stop()
202
-
203
- for {
204
- select {
205
- case <-c.ctx.Done():
206
- return
207
- case <-ticker.C:
208
- state, err := c.sync(false)
209
- if c.opt.AfterSync != nil {
210
- c.opt.AfterSync(state, err)
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
- "io"
6
- "time"
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
- var _ = Describe("Consumer", func() {
16
- var subject feedx.Consumer
17
- var obj *bfs.Object
18
- var ctx = context.Background()
19
-
20
- consume := func(r *feedx.Reader) (interface{}, error) {
21
- var msgs []*testdata.MockMessage
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
- BeforeEach(func() {
35
- obj = bfs.NewInMemObject("path/to/file.jsonz")
36
- Expect(writeMulti(obj, 2, mockTime)).To(Succeed())
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
- var err error
39
- subject, err = feedx.NewConsumerForRemote(ctx, obj, nil, consume)
40
- Expect(err).NotTo(HaveOccurred())
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
- AfterEach(func() {
44
- Expect(subject.Close()).To(Succeed())
45
- })
45
+ t.Run("always if no version", func(t *testing.T) {
46
+ csm := fixConsumer(t, 0)
47
+ defer csm.Close()
46
48
 
47
- It("syncs/retrieves feeds from remote", func() {
48
- Expect(subject.LastSync()).To(BeTemporally("~", time.Now(), time.Second))
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
- It("consumes feeds only if necessary", func() {
57
- prevSync := subject.LastSync()
58
- time.Sleep(2 * time.Millisecond)
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
- testable := subject.(interface{ TestSync() error })
61
- Expect(testable.TestSync()).To(Succeed())
62
- Expect(subject.LastSync()).To(BeTemporally(">", prevSync))
63
- Expect(subject.LastConsumed()).To(BeTemporally("==", prevSync)) // skipped on last sync
64
- Expect(subject.LastModified()).To(BeTemporally("==", mockTime.Truncate(time.Millisecond)))
65
- Expect(subject.NumRead()).To(Equal(2))
70
+ // second attempt
71
+ _ = testConsume(t, csm, &feedx.Status{
72
+ LocalVersion: 101,
73
+ RemoteVersion: 101,
74
+ Skipped: true,
75
+ })
66
76
  })
67
77
 
68
- It("always consumes if LastModified not set", func() {
69
- noModTime := bfs.NewInMemObject("path/to/file.json")
70
- Expect(writeMulti(noModTime, 2, time.Time{})).To(Succeed())
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
- csmr, err := feedx.NewConsumerForRemote(ctx, noModTime, nil, consume)
73
- Expect(err).NotTo(HaveOccurred())
96
+ func fixIncrementalConsumer(t *testing.T, version int64) feedx.Consumer {
97
+ t.Helper()
74
98
 
75
- prevSync := csmr.LastSync()
76
- time.Sleep(2 * time.Millisecond)
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
- testable := csmr.(interface{ TestSync() error })
79
- Expect(testable.TestSync()).To(Succeed())
80
- Expect(csmr.LastSync()).To(BeTemporally(">", prevSync))
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.12.7'
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.7'
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