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/reader_test.go
CHANGED
|
@@ -1,56 +1,107 @@
|
|
|
1
1
|
package feedx_test
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"context"
|
|
5
4
|
"io"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
5
|
+
"reflect"
|
|
6
|
+
"testing"
|
|
8
7
|
|
|
9
8
|
"github.com/bsm/bfs"
|
|
10
9
|
"github.com/bsm/feedx"
|
|
11
10
|
"github.com/bsm/feedx/internal/testdata"
|
|
12
|
-
. "github.com/bsm/ginkgo"
|
|
13
|
-
. "github.com/bsm/gomega"
|
|
14
11
|
)
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
var ctx = context.Background()
|
|
13
|
+
func TestReader(t *testing.T) {
|
|
14
|
+
t.Run("reads", func(t *testing.T) {
|
|
15
|
+
r := fixReader(t)
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
if data, err := io.ReadAll(r); err != nil {
|
|
18
|
+
t.Fatal("unexpected error", err)
|
|
19
|
+
} else if exp, got := 111, len(data); exp != got {
|
|
20
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
21
|
+
} else if exp, got := int64(0), r.NumRead(); exp != got {
|
|
22
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
t.Run("decodes", func(t *testing.T) {
|
|
27
|
+
r := fixReader(t)
|
|
28
|
+
msgs := drainReader(t, r)
|
|
29
|
+
if exp := seedN(3); !reflect.DeepEqual(exp, msgs) {
|
|
30
|
+
t.Errorf("expected %#v, got %#v", exp, msgs)
|
|
31
|
+
}
|
|
32
|
+
if exp, got := int64(3), r.NumRead(); exp != got {
|
|
33
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
34
|
+
}
|
|
28
35
|
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func fixReader(t *testing.T) *feedx.Reader {
|
|
39
|
+
t.Helper()
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
obj := bfs.NewInMemObject("path/to/file.jsonz")
|
|
42
|
+
if err := writeN(obj, 3, 0); err != nil {
|
|
43
|
+
t.Fatal("unexpected error", err)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
r, err := feedx.NewReader(t.Context(), obj, nil)
|
|
47
|
+
if err != nil {
|
|
48
|
+
t.Fatal("unexpected error", err)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
t.Cleanup(func() {
|
|
52
|
+
_ = r.Close()
|
|
32
53
|
})
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
55
|
+
return r
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func TestMultiReader(t *testing.T) {
|
|
59
|
+
t.Run("reads", func(t *testing.T) {
|
|
60
|
+
r := fixMultiReader(t)
|
|
61
|
+
|
|
62
|
+
if data, err := io.ReadAll(r); err != nil {
|
|
63
|
+
t.Fatal("unexpected error", err)
|
|
64
|
+
} else if exp, got := 222, len(data); exp != got {
|
|
65
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
66
|
+
} else if exp, got := int64(0), r.NumRead(); exp != got {
|
|
67
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
68
|
+
}
|
|
39
69
|
})
|
|
40
70
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Expect(err).NotTo(HaveOccurred())
|
|
50
|
-
msgs = append(msgs, &msg)
|
|
71
|
+
t.Run("decodes", func(t *testing.T) {
|
|
72
|
+
r := fixMultiReader(t)
|
|
73
|
+
msgs := drainReader(t, r)
|
|
74
|
+
if exp := seedN(6); !reflect.DeepEqual(exp, msgs) {
|
|
75
|
+
t.Errorf("expected %#v, got %#v", exp, msgs)
|
|
76
|
+
}
|
|
77
|
+
if exp, got := int64(6), r.NumRead(); exp != got {
|
|
78
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
51
79
|
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func fixMultiReader(t *testing.T) *feedx.Reader {
|
|
84
|
+
t.Helper()
|
|
52
85
|
|
|
53
|
-
|
|
54
|
-
|
|
86
|
+
obj := bfs.NewInMemObject("path/to/file.jsonz")
|
|
87
|
+
if err := writeN(obj, 3, 0); err != nil {
|
|
88
|
+
t.Fatal("unexpected error", err)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
r := feedx.MultiReader(t.Context(), []*bfs.Object{obj, obj}, nil)
|
|
92
|
+
t.Cleanup(func() {
|
|
93
|
+
_ = r.Close()
|
|
55
94
|
})
|
|
56
|
-
|
|
95
|
+
|
|
96
|
+
return r
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func drainReader(t *testing.T, r interface{ Decode(any) error }) []*testdata.MockMessage {
|
|
100
|
+
t.Helper()
|
|
101
|
+
|
|
102
|
+
msgs, err := readMessages(r)
|
|
103
|
+
if err != nil {
|
|
104
|
+
t.Fatal("unexpected error", err)
|
|
105
|
+
}
|
|
106
|
+
return msgs
|
|
107
|
+
}
|
data/scheduler.go
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
package feedx
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"sync"
|
|
6
|
+
"time"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// BeforeHook callbacks are run before jobs are started. It receives the local
|
|
10
|
+
// version before sync as an argument and may return false to abort the cycle.
|
|
11
|
+
type BeforeHook func(version int64) bool
|
|
12
|
+
|
|
13
|
+
// AfterHook callbacks are run after jobs have finished.
|
|
14
|
+
type AfterHook func(*Status, error)
|
|
15
|
+
|
|
16
|
+
// VersionCheck callbacks return the latest local version.
|
|
17
|
+
type VersionCheck func(context.Context) (int64, error)
|
|
18
|
+
|
|
19
|
+
// Scheduler runs cronjobs in regular intervals.
|
|
20
|
+
type Scheduler struct {
|
|
21
|
+
ctx context.Context
|
|
22
|
+
interval time.Duration
|
|
23
|
+
|
|
24
|
+
readerOpt *ReaderOptions
|
|
25
|
+
writerOpt *WriterOptions
|
|
26
|
+
versionCheck VersionCheck
|
|
27
|
+
|
|
28
|
+
// hooks
|
|
29
|
+
beforeHooks []BeforeHook
|
|
30
|
+
afterHooks []AfterHook
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Every creates a scheduler.
|
|
34
|
+
func Every(interval time.Duration) *Scheduler {
|
|
35
|
+
return &Scheduler{ctx: context.Background(), interval: interval}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// WithContext sets a custom context for the run.
|
|
39
|
+
func (s *Scheduler) WithContext(ctx context.Context) *Scheduler {
|
|
40
|
+
s.ctx = ctx
|
|
41
|
+
return s
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// BeforeSync adds custom before hooks.
|
|
45
|
+
func (s *Scheduler) BeforeSync(hooks ...BeforeHook) *Scheduler {
|
|
46
|
+
s.beforeHooks = append(s.beforeHooks, hooks...)
|
|
47
|
+
return s
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// AfterSync adds before hooks.
|
|
51
|
+
func (s *Scheduler) AfterSync(hooks ...AfterHook) *Scheduler {
|
|
52
|
+
s.afterHooks = append(s.afterHooks, hooks...)
|
|
53
|
+
return s
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// WithReaderOptions sets custom reader options for consumers.
|
|
57
|
+
func (s *Scheduler) WithReaderOptions(opt *ReaderOptions) *Scheduler {
|
|
58
|
+
s.readerOpt = opt
|
|
59
|
+
return s
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Consume starts a consumer job.
|
|
63
|
+
func (s *Scheduler) Consume(csm Consumer, cfn ConsumeFunc) *CronJob {
|
|
64
|
+
return newCronJob(s.ctx, s.interval, func(ctx context.Context) {
|
|
65
|
+
version := csm.Version()
|
|
66
|
+
if !s.runBeforeHooks(version) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
status, err := csm.Consume(ctx, s.readerOpt, cfn)
|
|
71
|
+
s.runAfterHooks(status, err)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// WithWriterOptions sets custom writer options for producers.
|
|
76
|
+
func (s *Scheduler) WithWriterOptions(opt *WriterOptions) *Scheduler {
|
|
77
|
+
s.writerOpt = opt
|
|
78
|
+
return s
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// WithVersionCheck sets a custom version check for producers.
|
|
82
|
+
func (s *Scheduler) WithVersionCheck(fn VersionCheck) *Scheduler {
|
|
83
|
+
s.versionCheck = fn
|
|
84
|
+
return s
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Produce starts a producer job.
|
|
88
|
+
func (s *Scheduler) Produce(pcr *Producer, pfn ProduceFunc) *CronJob {
|
|
89
|
+
return s.produce(func(ctx context.Context, version int64) (*Status, error) {
|
|
90
|
+
return pcr.Produce(ctx, version, s.writerOpt, pfn)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ProduceIncrementally starts an incremental producer job.
|
|
95
|
+
func (s *Scheduler) ProduceIncrementally(pcr *IncrementalProducer, pfn IncrementalProduceFunc) *CronJob {
|
|
96
|
+
return s.produce(func(ctx context.Context, version int64) (*Status, error) {
|
|
97
|
+
return pcr.Produce(ctx, version, s.writerOpt, pfn)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func (s *Scheduler) produce(fn func(context.Context, int64) (*Status, error)) *CronJob {
|
|
102
|
+
return newCronJob(s.ctx, s.interval, func(ctx context.Context) {
|
|
103
|
+
var version int64
|
|
104
|
+
if s.versionCheck != nil {
|
|
105
|
+
latest, err := s.versionCheck(s.ctx)
|
|
106
|
+
if err != nil {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
version = latest
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if !s.runBeforeHooks(version) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
status, err := fn(ctx, version)
|
|
117
|
+
s.runAfterHooks(status, err)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func (s *Scheduler) runBeforeHooks(version int64) bool {
|
|
122
|
+
for _, hook := range s.beforeHooks {
|
|
123
|
+
if !hook(version) {
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func (s *Scheduler) runAfterHooks(status *Status, err error) {
|
|
131
|
+
for _, hook := range s.afterHooks {
|
|
132
|
+
hook(status, err)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// CronJob runs in regular intervals until it's stopped.
|
|
137
|
+
type CronJob struct {
|
|
138
|
+
ctx context.Context
|
|
139
|
+
cancel context.CancelFunc
|
|
140
|
+
interval time.Duration
|
|
141
|
+
perform func(context.Context)
|
|
142
|
+
wait sync.WaitGroup
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func newCronJob(ctx context.Context, interval time.Duration, perform func(context.Context)) *CronJob {
|
|
146
|
+
ctx, cancel := context.WithCancel(ctx)
|
|
147
|
+
|
|
148
|
+
job := &CronJob{ctx: ctx, cancel: cancel, interval: interval, perform: perform}
|
|
149
|
+
job.perform(ctx) // perform immediately
|
|
150
|
+
|
|
151
|
+
job.wait.Add(1)
|
|
152
|
+
go job.loop()
|
|
153
|
+
return job
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Stop stops the job and waits until it is complete.
|
|
157
|
+
func (j *CronJob) Stop() {
|
|
158
|
+
j.cancel()
|
|
159
|
+
j.wait.Wait()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func (j *CronJob) loop() {
|
|
163
|
+
defer j.wait.Done()
|
|
164
|
+
|
|
165
|
+
ticker := time.NewTicker(j.interval)
|
|
166
|
+
defer ticker.Stop()
|
|
167
|
+
|
|
168
|
+
for {
|
|
169
|
+
select {
|
|
170
|
+
case <-j.ctx.Done():
|
|
171
|
+
return
|
|
172
|
+
case <-ticker.C:
|
|
173
|
+
j.perform(j.ctx)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
data/scheduler_test.go
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
package feedx_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"sync/atomic"
|
|
7
|
+
"testing"
|
|
8
|
+
"time"
|
|
9
|
+
|
|
10
|
+
"github.com/bsm/bfs"
|
|
11
|
+
"github.com/bsm/feedx"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestScheduler(t *testing.T) {
|
|
15
|
+
beforeCallbacks := new(atomic.Int32)
|
|
16
|
+
afterCallbacks := new(atomic.Int32)
|
|
17
|
+
numCycles := new(atomic.Int32)
|
|
18
|
+
numErrors := new(atomic.Int32)
|
|
19
|
+
|
|
20
|
+
obj := bfs.NewInMemObject("file.json")
|
|
21
|
+
defer obj.Close()
|
|
22
|
+
|
|
23
|
+
t.Run("produce", func(t *testing.T) {
|
|
24
|
+
pcr := feedx.NewProducerForRemote(obj)
|
|
25
|
+
defer pcr.Close()
|
|
26
|
+
|
|
27
|
+
job := feedx.Every(time.Millisecond).
|
|
28
|
+
BeforeSync(func(_ int64) bool {
|
|
29
|
+
beforeCallbacks.Add(1)
|
|
30
|
+
return true
|
|
31
|
+
}).
|
|
32
|
+
AfterSync(func(_ *feedx.Status, err error) {
|
|
33
|
+
afterCallbacks.Add(1)
|
|
34
|
+
|
|
35
|
+
if err != nil {
|
|
36
|
+
numErrors.Add(1)
|
|
37
|
+
}
|
|
38
|
+
}).
|
|
39
|
+
WithVersionCheck(func(_ context.Context) (int64, error) {
|
|
40
|
+
return 101, nil
|
|
41
|
+
}).
|
|
42
|
+
Produce(pcr, func(w *feedx.Writer) error {
|
|
43
|
+
if numCycles.Add(1)%2 == 0 {
|
|
44
|
+
return fmt.Errorf("failed!")
|
|
45
|
+
}
|
|
46
|
+
return nil
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
time.Sleep(5 * time.Millisecond)
|
|
50
|
+
job.Stop()
|
|
51
|
+
time.Sleep(2 * time.Millisecond)
|
|
52
|
+
|
|
53
|
+
ranTimes := numCycles.Load()
|
|
54
|
+
if min, got := 4, int(ranTimes); got <= min {
|
|
55
|
+
t.Errorf("expected %d >= %d", got, min)
|
|
56
|
+
}
|
|
57
|
+
if exp, got := ranTimes, beforeCallbacks.Load(); exp != got {
|
|
58
|
+
t.Errorf("expected %d, got %d", exp, got)
|
|
59
|
+
}
|
|
60
|
+
if exp, got := ranTimes, afterCallbacks.Load(); exp != got {
|
|
61
|
+
t.Errorf("expected %d, got %d", exp, got)
|
|
62
|
+
}
|
|
63
|
+
if exp, got := ranTimes/2, numErrors.Load(); exp != got {
|
|
64
|
+
t.Errorf("expected %d, got %d", exp, got)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// wait a little longer, make sure job was stopped
|
|
68
|
+
time.Sleep(2 * time.Millisecond)
|
|
69
|
+
if exp, got := ranTimes, numCycles.Load(); exp != got {
|
|
70
|
+
t.Errorf("expected %d, got %d", exp, got)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
beforeCallbacks.Store(0)
|
|
75
|
+
afterCallbacks.Store(0)
|
|
76
|
+
numCycles.Store(0)
|
|
77
|
+
numErrors.Store(0)
|
|
78
|
+
|
|
79
|
+
t.Run("consume", func(t *testing.T) {
|
|
80
|
+
|
|
81
|
+
csm := feedx.NewConsumerForRemote(obj)
|
|
82
|
+
defer csm.Close()
|
|
83
|
+
|
|
84
|
+
job := feedx.Every(time.Millisecond).
|
|
85
|
+
BeforeSync(func(_ int64) bool {
|
|
86
|
+
beforeCallbacks.Add(1)
|
|
87
|
+
return true
|
|
88
|
+
}).
|
|
89
|
+
AfterSync(func(_ *feedx.Status, err error) {
|
|
90
|
+
afterCallbacks.Add(1)
|
|
91
|
+
|
|
92
|
+
if err != nil {
|
|
93
|
+
numErrors.Add(1)
|
|
94
|
+
}
|
|
95
|
+
}).
|
|
96
|
+
Consume(csm, func(ctx context.Context, r *feedx.Reader) error {
|
|
97
|
+
if numCycles.Add(1)%2 == 0 {
|
|
98
|
+
return fmt.Errorf("failed!")
|
|
99
|
+
}
|
|
100
|
+
return nil
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
time.Sleep(5 * time.Millisecond)
|
|
104
|
+
job.Stop()
|
|
105
|
+
time.Sleep(2 * time.Millisecond)
|
|
106
|
+
|
|
107
|
+
ranTimes := numCycles.Load()
|
|
108
|
+
if min, got := 4, int(ranTimes); got <= min {
|
|
109
|
+
t.Errorf("expected %d >= %d", got, min)
|
|
110
|
+
}
|
|
111
|
+
if exp, got := ranTimes, beforeCallbacks.Load(); exp != got {
|
|
112
|
+
t.Errorf("expected %d, got %d", exp, got)
|
|
113
|
+
}
|
|
114
|
+
if exp, got := ranTimes, afterCallbacks.Load(); exp != got {
|
|
115
|
+
t.Errorf("expected %d, got %d", exp, got)
|
|
116
|
+
}
|
|
117
|
+
if exp, got := ranTimes/2, numErrors.Load(); exp != got {
|
|
118
|
+
t.Errorf("expected %d, got %d", exp, got)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// wait a little longer, make sure job was stopped
|
|
122
|
+
time.Sleep(2 * time.Millisecond)
|
|
123
|
+
if exp, got := ranTimes, numCycles.Load(); exp != got {
|
|
124
|
+
t.Errorf("expected %d, got %d", exp, got)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
}
|
data/writer.go
CHANGED
|
@@ -3,8 +3,9 @@ package feedx
|
|
|
3
3
|
import (
|
|
4
4
|
"bufio"
|
|
5
5
|
"context"
|
|
6
|
+
"errors"
|
|
6
7
|
"io"
|
|
7
|
-
"
|
|
8
|
+
"strconv"
|
|
8
9
|
|
|
9
10
|
"github.com/bsm/bfs"
|
|
10
11
|
)
|
|
@@ -19,9 +20,9 @@ type WriterOptions struct {
|
|
|
19
20
|
// Default: auto-detected from URL path.
|
|
20
21
|
Compression Compression
|
|
21
22
|
|
|
22
|
-
// Provides an optional
|
|
23
|
-
// Default:
|
|
24
|
-
|
|
23
|
+
// Provides an optional version which is stored with the remote metadata.
|
|
24
|
+
// Default: 0
|
|
25
|
+
Version int64
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
func (o *WriterOptions) norm(name string) {
|
|
@@ -38,7 +39,7 @@ type Writer struct {
|
|
|
38
39
|
ctx context.Context
|
|
39
40
|
remote *bfs.Object
|
|
40
41
|
opt WriterOptions
|
|
41
|
-
num
|
|
42
|
+
num int64
|
|
42
43
|
|
|
43
44
|
bw bfs.Writer
|
|
44
45
|
cw io.WriteCloser // compression writer
|
|
@@ -100,7 +101,7 @@ func (w *Writer) Encode(v interface{}) error {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
// NumWritten returns the number of written values.
|
|
103
|
-
func (w *Writer) NumWritten()
|
|
104
|
+
func (w *Writer) NumWritten() int64 {
|
|
104
105
|
return w.num
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -109,7 +110,7 @@ func (w *Writer) Discard() error {
|
|
|
109
110
|
err := w.close()
|
|
110
111
|
if w.bw != nil {
|
|
111
112
|
if e := w.bw.Discard(); e != nil {
|
|
112
|
-
err = e
|
|
113
|
+
err = errors.Join(err, e)
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
return err
|
|
@@ -120,7 +121,7 @@ func (w *Writer) Commit() error {
|
|
|
120
121
|
err := w.close()
|
|
121
122
|
if w.bw != nil {
|
|
122
123
|
if e := w.bw.Commit(); e != nil {
|
|
123
|
-
err = e
|
|
124
|
+
err = errors.Join(err, e)
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
return err
|
|
@@ -129,17 +130,17 @@ func (w *Writer) Commit() error {
|
|
|
129
130
|
func (w *Writer) close() (err error) {
|
|
130
131
|
if w.fe != nil {
|
|
131
132
|
if e := w.fe.Close(); e != nil {
|
|
132
|
-
err = e
|
|
133
|
+
err = errors.Join(err, e)
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
if w.ww != nil {
|
|
136
137
|
if e := w.ww.Flush(); e != nil {
|
|
137
|
-
err = e
|
|
138
|
+
err = errors.Join(err, e)
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
if w.cw != nil {
|
|
141
142
|
if e := w.cw.Close(); e != nil {
|
|
142
|
-
err = e
|
|
143
|
+
err = errors.Join(err, e)
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
return err
|
|
@@ -147,9 +148,8 @@ func (w *Writer) close() (err error) {
|
|
|
147
148
|
|
|
148
149
|
func (w *Writer) ensureCreated() error {
|
|
149
150
|
if w.bw == nil {
|
|
150
|
-
ts := timestampFromTime(w.opt.LastMod)
|
|
151
151
|
bw, err := w.remote.Create(w.ctx, &bfs.WriteOptions{
|
|
152
|
-
Metadata: bfs.Metadata{
|
|
152
|
+
Metadata: bfs.Metadata{metaVersion: strconv.FormatInt(w.opt.Version, 10)},
|
|
153
153
|
})
|
|
154
154
|
if err != nil {
|
|
155
155
|
return err
|
data/writer_test.go
CHANGED
|
@@ -2,66 +2,83 @@ package feedx_test
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"bytes"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"reflect"
|
|
6
|
+
"testing"
|
|
7
7
|
|
|
8
8
|
"github.com/bsm/bfs"
|
|
9
9
|
"github.com/bsm/feedx"
|
|
10
|
-
. "github.com/bsm/ginkgo"
|
|
11
|
-
. "github.com/bsm/gomega"
|
|
12
10
|
)
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
func TestWriter(t *testing.T) {
|
|
13
|
+
t.Run("writes plain", func(t *testing.T) {
|
|
14
|
+
obj := bfs.NewInMemObject("path/to/file.json")
|
|
15
|
+
info := testWriter(t, obj, &feedx.WriterOptions{
|
|
16
|
+
Version: 101,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
if exp, got := int64(10000), info.Size; exp != got {
|
|
20
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
21
|
+
}
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
meta := bfs.Metadata{"X-Feedx-Version": "101"}
|
|
24
|
+
if exp, got := meta, info.Metadata; !reflect.DeepEqual(exp, got) {
|
|
25
|
+
t.Errorf("expected %#v, got %#v", exp, got)
|
|
26
|
+
}
|
|
21
27
|
})
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
t.Run("writes compressed", func(t *testing.T) {
|
|
30
|
+
obj := bfs.NewInMemObject("path/to/file.jsonz")
|
|
31
|
+
info := testWriter(t, obj, &feedx.WriterOptions{
|
|
32
|
+
Version: 101,
|
|
26
33
|
})
|
|
27
|
-
defer w.Discard()
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
if max, got := int64(100), info.Size; got > max {
|
|
36
|
+
t.Errorf("expected %v to be < %v", got, max)
|
|
37
|
+
}
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
meta := bfs.Metadata{"X-Feedx-Version": "101"}
|
|
40
|
+
if exp, got := meta, info.Metadata; !reflect.DeepEqual(exp, got) {
|
|
41
|
+
t.Errorf("expected %#v, got %#v", exp, got)
|
|
42
|
+
}
|
|
36
43
|
})
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
t.Run("encodes", func(t *testing.T) {
|
|
46
|
+
obj := bfs.NewInMemObject("path/to/file.json")
|
|
47
|
+
if err := writeN(obj, 10, 101); err != nil {
|
|
48
|
+
t.Fatal("unexpected error", err)
|
|
49
|
+
}
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
info, err := obj.Head(t.Context())
|
|
52
|
+
if err != nil {
|
|
53
|
+
t.Fatal("unexpected error", err)
|
|
54
|
+
}
|
|
55
|
+
if exp, got := int64(370), info.Size; exp != got {
|
|
56
|
+
t.Errorf("expected %v, got %v", exp, got)
|
|
57
|
+
}
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
meta := bfs.Metadata{"X-Feedx-Version": "101"}
|
|
60
|
+
if exp, got := meta, info.Metadata; !reflect.DeepEqual(exp, got) {
|
|
61
|
+
t.Errorf("expected %#v, got %#v", exp, got)
|
|
62
|
+
}
|
|
51
63
|
})
|
|
64
|
+
}
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Expect(writeMulti(compressed, 10, mockTime)).To(Succeed())
|
|
66
|
+
func testWriter(t *testing.T, obj *bfs.Object, opts *feedx.WriterOptions) *bfs.MetaInfo {
|
|
67
|
+
t.Helper()
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Expect(info.Size).To(BeNumerically("~", 370, 10))
|
|
60
|
-
Expect(info.Metadata).To(Equal(bfs.Metadata{"X-Feedx-Last-Modified": "0"}))
|
|
69
|
+
w := feedx.NewWriter(t.Context(), obj, opts)
|
|
70
|
+
t.Cleanup(func() { _ = w.Discard() })
|
|
61
71
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
72
|
+
if _, err := w.Write(bytes.Repeat([]byte{'x'}, 10000)); err != nil {
|
|
73
|
+
t.Fatal("unexpected error", err)
|
|
74
|
+
}
|
|
75
|
+
if err := w.Commit(); err != nil {
|
|
76
|
+
t.Fatal("unexpected error", err)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
info, err := obj.Head(t.Context())
|
|
80
|
+
if err != nil {
|
|
81
|
+
t.Fatal("unexpected error", err)
|
|
82
|
+
}
|
|
83
|
+
return info
|
|
84
|
+
}
|