github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/lib/batcher/batcher_test.go (about) 1 package batcher 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "sync/atomic" 9 "testing" 10 "time" 11 12 "github.com/rclone/rclone/fs" 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 ) 16 17 type ( 18 Result string 19 Item string 20 ) 21 22 func TestBatcherNew(t *testing.T) { 23 ctx := context.Background() 24 ci := fs.GetConfig(ctx) 25 26 opt := Options{ 27 Mode: "async", 28 Size: 100, 29 Timeout: 1 * time.Second, 30 MaxBatchSize: 1000, 31 DefaultTimeoutSync: 500 * time.Millisecond, 32 DefaultTimeoutAsync: 10 * time.Second, 33 DefaultBatchSizeAsync: 100, 34 } 35 commitBatch := func(ctx context.Context, items []Item, results []Result, errors []error) (err error) { 36 return nil 37 } 38 39 b, err := New[Item, Result](ctx, nil, commitBatch, opt) 40 require.NoError(t, err) 41 require.True(t, b.Batching()) 42 b.Shutdown() 43 44 opt.Mode = "sync" 45 b, err = New[Item, Result](ctx, nil, commitBatch, opt) 46 require.NoError(t, err) 47 require.True(t, b.Batching()) 48 b.Shutdown() 49 50 opt.Mode = "off" 51 b, err = New[Item, Result](ctx, nil, commitBatch, opt) 52 require.NoError(t, err) 53 require.False(t, b.Batching()) 54 b.Shutdown() 55 56 opt.Mode = "bad" 57 _, err = New[Item, Result](ctx, nil, commitBatch, opt) 58 require.ErrorContains(t, err, "batch mode") 59 60 opt.Mode = "async" 61 opt.Size = opt.MaxBatchSize + 1 62 _, err = New[Item, Result](ctx, nil, commitBatch, opt) 63 require.ErrorContains(t, err, "batch size") 64 65 opt.Mode = "sync" 66 opt.Size = 0 67 opt.Timeout = 0 68 b, err = New[Item, Result](ctx, nil, commitBatch, opt) 69 require.NoError(t, err) 70 assert.Equal(t, ci.Transfers, b.opt.Size) 71 assert.Equal(t, opt.DefaultTimeoutSync, b.opt.Timeout) 72 b.Shutdown() 73 74 opt.Mode = "async" 75 opt.Size = 0 76 opt.Timeout = 0 77 b, err = New[Item, Result](ctx, nil, commitBatch, opt) 78 require.NoError(t, err) 79 assert.Equal(t, opt.DefaultBatchSizeAsync, b.opt.Size) 80 assert.Equal(t, opt.DefaultTimeoutAsync, b.opt.Timeout) 81 b.Shutdown() 82 83 // Check we get an error on commit 84 _, err = b.Commit(ctx, "last", Item("last")) 85 require.ErrorContains(t, err, "shutting down") 86 87 } 88 89 func TestBatcherCommit(t *testing.T) { 90 ctx := context.Background() 91 92 opt := Options{ 93 Mode: "sync", 94 Size: 3, 95 Timeout: 1 * time.Second, 96 MaxBatchSize: 1000, 97 DefaultTimeoutSync: 500 * time.Millisecond, 98 DefaultTimeoutAsync: 10 * time.Second, 99 DefaultBatchSizeAsync: 100, 100 } 101 var wg sync.WaitGroup 102 errFail := errors.New("fail") 103 var commits int 104 var totalSize int 105 commitBatch := func(ctx context.Context, items []Item, results []Result, errors []error) (err error) { 106 commits += 1 107 totalSize += len(items) 108 for i := range items { 109 if items[i] == "5" { 110 errors[i] = errFail 111 } else { 112 results[i] = Result(items[i]) + " result" 113 } 114 } 115 return nil 116 } 117 b, err := New[Item, Result](ctx, nil, commitBatch, opt) 118 require.NoError(t, err) 119 defer b.Shutdown() 120 121 for i := 0; i < 10; i++ { 122 wg.Add(1) 123 s := fmt.Sprintf("%d", i) 124 go func() { 125 defer wg.Done() 126 result, err := b.Commit(ctx, s, Item(s)) 127 if s == "5" { 128 assert.True(t, errors.Is(err, errFail)) 129 } else { 130 require.NoError(t, err) 131 assert.Equal(t, Result(s+" result"), result) 132 } 133 }() 134 } 135 wg.Wait() 136 assert.Equal(t, 4, commits) 137 assert.Equal(t, 10, totalSize) 138 } 139 140 func TestBatcherCommitFail(t *testing.T) { 141 ctx := context.Background() 142 143 opt := Options{ 144 Mode: "sync", 145 Size: 3, 146 Timeout: 1 * time.Second, 147 MaxBatchSize: 1000, 148 DefaultTimeoutSync: 500 * time.Millisecond, 149 DefaultTimeoutAsync: 10 * time.Second, 150 DefaultBatchSizeAsync: 100, 151 } 152 var wg sync.WaitGroup 153 errFail := errors.New("fail") 154 var commits int 155 var totalSize int 156 commitBatch := func(ctx context.Context, items []Item, results []Result, errors []error) (err error) { 157 commits += 1 158 totalSize += len(items) 159 return errFail 160 } 161 b, err := New[Item, Result](ctx, nil, commitBatch, opt) 162 require.NoError(t, err) 163 defer b.Shutdown() 164 165 for i := 0; i < 10; i++ { 166 wg.Add(1) 167 s := fmt.Sprintf("%d", i) 168 go func() { 169 defer wg.Done() 170 _, err := b.Commit(ctx, s, Item(s)) 171 assert.True(t, errors.Is(err, errFail)) 172 }() 173 } 174 wg.Wait() 175 assert.Equal(t, 4, commits) 176 assert.Equal(t, 10, totalSize) 177 } 178 179 func TestBatcherCommitShutdown(t *testing.T) { 180 ctx := context.Background() 181 182 opt := Options{ 183 Mode: "sync", 184 Size: 3, 185 Timeout: 1 * time.Second, 186 MaxBatchSize: 1000, 187 DefaultTimeoutSync: 500 * time.Millisecond, 188 DefaultTimeoutAsync: 10 * time.Second, 189 DefaultBatchSizeAsync: 100, 190 } 191 var wg sync.WaitGroup 192 var commits int 193 var totalSize int 194 commitBatch := func(ctx context.Context, items []Item, results []Result, errors []error) (err error) { 195 commits += 1 196 totalSize += len(items) 197 for i := range items { 198 results[i] = Result(items[i]) 199 } 200 return nil 201 } 202 b, err := New[Item, Result](ctx, nil, commitBatch, opt) 203 require.NoError(t, err) 204 205 for i := 0; i < 10; i++ { 206 wg.Add(1) 207 s := fmt.Sprintf("%d", i) 208 go func() { 209 defer wg.Done() 210 result, err := b.Commit(ctx, s, Item(s)) 211 assert.NoError(t, err) 212 assert.Equal(t, Result(s), result) 213 }() 214 } 215 216 time.Sleep(100 * time.Millisecond) 217 b.Shutdown() // shutdown with batches outstanding 218 219 wg.Wait() 220 assert.Equal(t, 4, commits) 221 assert.Equal(t, 10, totalSize) 222 } 223 224 func TestBatcherCommitAsync(t *testing.T) { 225 ctx := context.Background() 226 227 opt := Options{ 228 Mode: "async", 229 Size: 3, 230 Timeout: 1 * time.Second, 231 MaxBatchSize: 1000, 232 DefaultTimeoutSync: 500 * time.Millisecond, 233 DefaultTimeoutAsync: 10 * time.Second, 234 DefaultBatchSizeAsync: 100, 235 } 236 var wg sync.WaitGroup 237 errFail := errors.New("fail") 238 var commits atomic.Int32 239 var totalSize atomic.Int32 240 commitBatch := func(ctx context.Context, items []Item, results []Result, errors []error) (err error) { 241 wg.Add(1) 242 defer wg.Done() 243 // t.Logf("commit %d", len(items)) 244 commits.Add(1) 245 totalSize.Add(int32(len(items))) 246 for i := range items { 247 if items[i] == "5" { 248 errors[i] = errFail 249 } else { 250 results[i] = Result(items[i]) + " result" 251 } 252 } 253 return nil 254 } 255 b, err := New[Item, Result](ctx, nil, commitBatch, opt) 256 require.NoError(t, err) 257 defer b.Shutdown() 258 259 for i := 0; i < 10; i++ { 260 wg.Add(1) 261 s := fmt.Sprintf("%d", i) 262 go func() { 263 defer wg.Done() 264 result, err := b.Commit(ctx, s, Item(s)) 265 // Async just returns straight away 266 require.NoError(t, err) 267 assert.Equal(t, Result(""), result) 268 }() 269 } 270 time.Sleep(2 * time.Second) // wait for batch timeout - needed with async 271 wg.Wait() 272 273 assert.Equal(t, int32(4), commits.Load()) 274 assert.Equal(t, int32(10), totalSize.Load()) 275 }