github.com/opensearch-project/opensearch-go/v2@v2.3.0/opensearchutil/bulk_indexer_internal_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // 3 // The OpenSearch Contributors require contributions made to 4 // this file be licensed under the Apache-2.0 license or a 5 // compatible open source license. 6 // 7 // Modifications Copyright OpenSearch Contributors. See 8 // GitHub history for details. 9 10 // Licensed to Elasticsearch B.V. under one or more contributor 11 // license agreements. See the NOTICE file distributed with 12 // this work for additional information regarding copyright 13 // ownership. Elasticsearch B.V. licenses this file to you under 14 // the Apache License, Version 2.0 (the "License"); you may 15 // not use this file except in compliance with the License. 16 // You may obtain a copy of the License at 17 // 18 // http://www.apache.org/licenses/LICENSE-2.0 19 // 20 // Unless required by applicable law or agreed to in writing, 21 // software distributed under the License is distributed on an 22 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 23 // KIND, either express or implied. See the License for the 24 // specific language governing permissions and limitations 25 // under the License. 26 27 //go:build !integration 28 // +build !integration 29 30 package opensearchutil 31 32 import ( 33 "bytes" 34 "context" 35 "encoding/json" 36 "fmt" 37 "io" 38 "io/ioutil" 39 "log" 40 "net/http" 41 "os" 42 "reflect" 43 "strconv" 44 "strings" 45 "sync" 46 "sync/atomic" 47 "testing" 48 "time" 49 50 "github.com/opensearch-project/opensearch-go/v2" 51 "github.com/opensearch-project/opensearch-go/v2/opensearchtransport" 52 ) 53 54 var infoBody = `{ 55 "version" : { 56 "number" : "1.0.0", 57 "distribution" : "opensearch" 58 } 59 }` 60 61 var defaultRoundTripFunc = func(*http.Request) (*http.Response, error) { 62 return &http.Response{Body: ioutil.NopCloser(strings.NewReader(`{}`))}, nil 63 } 64 65 type mockTransport struct { 66 RoundTripFunc func(*http.Request) (*http.Response, error) 67 } 68 69 func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { 70 if t.RoundTripFunc == nil { 71 return defaultRoundTripFunc(req) 72 } 73 return t.RoundTripFunc(req) 74 } 75 76 func TestBulkIndexer(t *testing.T) { 77 t.Run("Basic", func(t *testing.T) { 78 var ( 79 wg sync.WaitGroup 80 81 countReqs int 82 testfile string 83 numItems = 6 84 ) 85 86 client, _ := opensearch.NewClient(opensearch.Config{Transport: &mockTransport{ 87 RoundTripFunc: func(request *http.Request) (*http.Response, error) { 88 if request.URL.Path == "/" { 89 return &http.Response{Header: http.Header{"Content-Type": []string{"application/json"}}, Body: ioutil.NopCloser(strings.NewReader(infoBody))}, nil 90 } 91 92 countReqs++ 93 switch countReqs { 94 case 1: 95 testfile = "testdata/bulk_response_1a.json" 96 case 2: 97 testfile = "testdata/bulk_response_1b.json" 98 case 3: 99 testfile = "testdata/bulk_response_1c.json" 100 } 101 bodyContent, _ := ioutil.ReadFile(testfile) 102 return &http.Response{Body: ioutil.NopCloser(bytes.NewBuffer(bodyContent))}, nil 103 }, 104 }}) 105 106 cfg := BulkIndexerConfig{ 107 NumWorkers: 1, 108 FlushBytes: 50, 109 FlushInterval: time.Hour, // Disable auto-flushing, because response doesn't match number of items 110 Client: client} 111 if os.Getenv("DEBUG") != "" { 112 cfg.DebugLogger = log.New(os.Stdout, "", 0) 113 } 114 115 bi, _ := NewBulkIndexer(cfg) 116 117 for i := 1; i <= numItems; i++ { 118 wg.Add(1) 119 go func(i int) { 120 defer wg.Done() 121 err := bi.Add(context.Background(), BulkIndexerItem{ 122 Action: "foo", 123 DocumentID: strconv.Itoa(i), 124 Body: strings.NewReader(fmt.Sprintf(`{"title":"foo-%d"}`, i)), 125 }) 126 if err != nil { 127 t.Errorf("Unexpected error: %s", err) 128 return 129 } 130 }(i) 131 } 132 wg.Wait() 133 134 if err := bi.Close(context.Background()); err != nil { 135 t.Errorf("Unexpected error: %s", err) 136 } 137 138 stats := bi.Stats() 139 140 // added = numitems 141 if stats.NumAdded != uint64(numItems) { 142 t.Errorf("Unexpected NumAdded: want=%d, got=%d", numItems, stats.NumAdded) 143 } 144 145 // flushed = numitems - 1x conflict + 1x not_found 146 if stats.NumFlushed != uint64(numItems-2) { 147 t.Errorf("Unexpected NumFlushed: want=%d, got=%d", numItems-2, stats.NumFlushed) 148 } 149 150 // failed = 1x conflict + 1x not_found 151 if stats.NumFailed != 2 { 152 t.Errorf("Unexpected NumFailed: want=%d, got=%d", 2, stats.NumFailed) 153 } 154 155 // indexed = 1x 156 if stats.NumIndexed != 1 { 157 t.Errorf("Unexpected NumIndexed: want=%d, got=%d", 1, stats.NumIndexed) 158 } 159 160 // created = 1x 161 if stats.NumCreated != 1 { 162 t.Errorf("Unexpected NumCreated: want=%d, got=%d", 1, stats.NumCreated) 163 } 164 165 // deleted = 1x 166 if stats.NumDeleted != 1 { 167 t.Errorf("Unexpected NumDeleted: want=%d, got=%d", 1, stats.NumDeleted) 168 } 169 170 if stats.NumUpdated != 1 { 171 t.Errorf("Unexpected NumUpdated: want=%d, got=%d", 1, stats.NumUpdated) 172 } 173 174 // 3 items * 40 bytes, 2 workers, 1 request per worker 175 if stats.NumRequests != 3 { 176 t.Errorf("Unexpected NumRequests: want=%d, got=%d", 3, stats.NumRequests) 177 } 178 }) 179 180 t.Run("Add() Timeout", func(t *testing.T) { 181 client, _ := opensearch.NewClient(opensearch.Config{Transport: &mockTransport{}}) 182 bi, _ := NewBulkIndexer(BulkIndexerConfig{NumWorkers: 1, Client: client}) 183 ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) 184 defer cancel() 185 time.Sleep(100 * time.Millisecond) 186 187 var errs []error 188 for i := 0; i < 10; i++ { 189 errs = append(errs, bi.Add(ctx, BulkIndexerItem{Action: "delete", DocumentID: "timeout"})) 190 } 191 if err := bi.Close(context.Background()); err != nil { 192 t.Errorf("Unexpected error: %s", err) 193 } 194 195 var gotError bool 196 for _, err := range errs { 197 if err != nil && err.Error() == "context deadline exceeded" { 198 gotError = true 199 } 200 } 201 if !gotError { 202 t.Errorf("Expected timeout error, but none in: %q", errs) 203 } 204 }) 205 206 t.Run("Close() Cancel", func(t *testing.T) { 207 client, _ := opensearch.NewClient(opensearch.Config{Transport: &mockTransport{}}) 208 bi, _ := NewBulkIndexer(BulkIndexerConfig{ 209 NumWorkers: 1, 210 FlushBytes: 1, 211 Client: client, 212 }) 213 214 for i := 0; i < 10; i++ { 215 bi.Add(context.Background(), BulkIndexerItem{Action: "foo"}) 216 } 217 218 ctx, cancel := context.WithCancel(context.Background()) 219 cancel() 220 if err := bi.Close(ctx); err == nil { 221 t.Errorf("Expected context cancelled error, but got: %v", err) 222 } 223 }) 224 225 t.Run("Indexer Callback", func(t *testing.T) { 226 config := opensearch.Config{ 227 Transport: &mockTransport{ 228 RoundTripFunc: func(request *http.Request) (*http.Response, error) { 229 if request.URL.Path == "/" { 230 return &http.Response{Body: ioutil.NopCloser(strings.NewReader(infoBody))}, nil 231 } 232 233 return nil, fmt.Errorf("Mock transport error") 234 }, 235 }, 236 } 237 if os.Getenv("DEBUG") != "" { 238 config.Logger = &opensearchtransport.ColorLogger{ 239 Output: os.Stdout, 240 EnableRequestBody: true, 241 EnableResponseBody: true, 242 } 243 } 244 245 client, _ := opensearch.NewClient(config) 246 247 var indexerError error 248 biCfg := BulkIndexerConfig{ 249 NumWorkers: 1, 250 Client: client, 251 OnError: func(ctx context.Context, err error) { indexerError = err }, 252 } 253 if os.Getenv("DEBUG") != "" { 254 biCfg.DebugLogger = log.New(os.Stdout, "", 0) 255 } 256 257 bi, _ := NewBulkIndexer(biCfg) 258 259 if err := bi.Add(context.Background(), BulkIndexerItem{ 260 Action: "foo", 261 }); err != nil { 262 t.Fatalf("Unexpected error: %s", err) 263 } 264 265 bi.Close(context.Background()) 266 267 if indexerError == nil { 268 t.Errorf("Expected indexerError to not be nil") 269 } 270 }) 271 272 t.Run("Item Callbacks", func(t *testing.T) { 273 var ( 274 countSuccessful uint64 275 countFailed uint64 276 failedIDs []string 277 successfulItemBodies []string 278 failedItemBodies []string 279 280 numItems = 4 281 numFailed = 2 282 bodyContent, _ = ioutil.ReadFile("testdata/bulk_response_2.json") 283 ) 284 285 client, _ := opensearch.NewClient(opensearch.Config{Transport: &mockTransport{ 286 RoundTripFunc: func(request *http.Request) (*http.Response, error) { 287 if request.URL.Path == "/" { 288 return &http.Response{ 289 StatusCode: http.StatusOK, 290 Status: "200 OK", 291 Body: ioutil.NopCloser(strings.NewReader(infoBody)), 292 Header: http.Header{"Content-Type": []string{"application/json"}}, 293 }, nil 294 } 295 296 return &http.Response{Body: ioutil.NopCloser(bytes.NewBuffer(bodyContent))}, nil 297 }, 298 }}) 299 300 cfg := BulkIndexerConfig{NumWorkers: 1, Client: client} 301 if os.Getenv("DEBUG") != "" { 302 cfg.DebugLogger = log.New(os.Stdout, "", 0) 303 } 304 305 bi, _ := NewBulkIndexer(cfg) 306 307 successFunc := func(ctx context.Context, item BulkIndexerItem, res BulkIndexerResponseItem) { 308 atomic.AddUint64(&countSuccessful, 1) 309 310 buf, err := ioutil.ReadAll(item.Body) 311 if err != nil { 312 t.Fatalf("Unexpected error: %s", err) 313 } 314 successfulItemBodies = append(successfulItemBodies, string(buf)) 315 } 316 failureFunc := func(ctx context.Context, item BulkIndexerItem, res BulkIndexerResponseItem, err error) { 317 if err != nil { 318 t.Fatalf("Unexpected error: %s", err) 319 } 320 atomic.AddUint64(&countFailed, 1) 321 failedIDs = append(failedIDs, item.DocumentID) 322 323 buf, err := ioutil.ReadAll(item.Body) 324 if err != nil { 325 t.Fatalf("Unexpected error: %s", err) 326 } 327 failedItemBodies = append(failedItemBodies, string(buf)) 328 } 329 330 if err := bi.Add(context.Background(), BulkIndexerItem{ 331 Action: "index", 332 DocumentID: "1", 333 Body: strings.NewReader(`{"title":"foo"}`), 334 OnSuccess: successFunc, 335 OnFailure: failureFunc, 336 }); err != nil { 337 t.Fatalf("Unexpected error: %s", err) 338 } 339 340 if err := bi.Add(context.Background(), BulkIndexerItem{ 341 Action: "create", 342 DocumentID: "1", 343 Body: strings.NewReader(`{"title":"bar"}`), 344 OnSuccess: successFunc, 345 OnFailure: failureFunc, 346 }); err != nil { 347 t.Fatalf("Unexpected error: %s", err) 348 } 349 350 if err := bi.Add(context.Background(), BulkIndexerItem{ 351 Action: "delete", 352 DocumentID: "2", 353 Body: strings.NewReader(`{"title":"baz"}`), 354 OnSuccess: successFunc, 355 OnFailure: failureFunc, 356 }); err != nil { 357 t.Fatalf("Unexpected error: %s", err) 358 } 359 360 if err := bi.Add(context.Background(), BulkIndexerItem{ 361 Action: "update", 362 DocumentID: "3", 363 Body: strings.NewReader(`{"doc":{"title":"qux"}}`), 364 OnSuccess: successFunc, 365 OnFailure: failureFunc, 366 }); err != nil { 367 t.Fatalf("Unexpected error: %s", err) 368 } 369 370 if err := bi.Close(context.Background()); err != nil { 371 t.Errorf("Unexpected error: %s", err) 372 } 373 374 stats := bi.Stats() 375 376 if stats.NumAdded != uint64(numItems) { 377 t.Errorf("Unexpected NumAdded: %d", stats.NumAdded) 378 } 379 380 // Two failures are expected: 381 // 382 // * Operation #2: document can't be created, because a document with the same ID already exists. 383 // * Operation #3: document can't be deleted, because it doesn't exist. 384 385 if stats.NumFailed != uint64(numFailed) { 386 t.Errorf("Unexpected NumFailed: %d", stats.NumFailed) 387 } 388 389 if stats.NumFlushed != 2 { 390 t.Errorf("Unexpected NumFailed: %d", stats.NumFailed) 391 } 392 393 if stats.NumIndexed != 1 { 394 t.Errorf("Unexpected NumIndexed: %d", stats.NumIndexed) 395 } 396 397 if stats.NumUpdated != 1 { 398 t.Errorf("Unexpected NumUpdated: %d", stats.NumUpdated) 399 } 400 401 if countSuccessful != uint64(numItems-numFailed) { 402 t.Errorf("Unexpected countSuccessful: %d", countSuccessful) 403 } 404 405 if countFailed != uint64(numFailed) { 406 t.Errorf("Unexpected countFailed: %d", countFailed) 407 } 408 409 if !reflect.DeepEqual(failedIDs, []string{"1", "2"}) { 410 t.Errorf("Unexpected failedIDs: %#v", failedIDs) 411 } 412 413 if !reflect.DeepEqual(successfulItemBodies, []string{`{"title":"foo"}`, `{"doc":{"title":"qux"}}`}) { 414 t.Errorf("Unexpected successfulItemBodies: %#v", successfulItemBodies) 415 } 416 417 if !reflect.DeepEqual(failedItemBodies, []string{`{"title":"bar"}`, `{"title":"baz"}`}) { 418 t.Errorf("Unexpected failedItemBodies: %#v", failedItemBodies) 419 } 420 }) 421 422 t.Run("OnFlush callbacks", func(t *testing.T) { 423 type contextKey string 424 client, _ := opensearch.NewClient(opensearch.Config{Transport: &mockTransport{}}) 425 bi, _ := NewBulkIndexer(BulkIndexerConfig{ 426 Client: client, 427 Index: "foo", 428 OnFlushStart: func(ctx context.Context) context.Context { 429 fmt.Println(">>> Flush started") 430 return context.WithValue(ctx, contextKey("start"), time.Now().UTC()) 431 }, 432 OnFlushEnd: func(ctx context.Context) { 433 var duration time.Duration 434 if v := ctx.Value("start"); v != nil { 435 duration = time.Since(v.(time.Time)) 436 } 437 fmt.Printf(">>> Flush finished (duration: %s)\n", duration) 438 }, 439 }) 440 441 err := bi.Add(context.Background(), BulkIndexerItem{ 442 Action: "index", 443 Body: strings.NewReader(`{"title":"foo"}`), 444 }) 445 if err != nil { 446 t.Fatalf("Unexpected error: %s", err) 447 } 448 449 if err := bi.Close(context.Background()); err != nil { 450 t.Errorf("Unexpected error: %s", err) 451 } 452 453 stats := bi.Stats() 454 455 if stats.NumAdded != uint64(1) { 456 t.Errorf("Unexpected NumAdded: %d", stats.NumAdded) 457 } 458 }) 459 460 t.Run("Automatic flush", func(t *testing.T) { 461 client, _ := opensearch.NewClient(opensearch.Config{Transport: &mockTransport{ 462 RoundTripFunc: func(request *http.Request) (*http.Response, error) { 463 if request.URL.Path == "/" { 464 return &http.Response{ 465 StatusCode: http.StatusOK, 466 Status: "200 OK", 467 Body: ioutil.NopCloser(strings.NewReader(infoBody)), 468 Header: http.Header{"Content-Type": []string{"application/json"}}, 469 }, nil 470 } 471 472 return &http.Response{ 473 StatusCode: http.StatusOK, 474 Status: "200 OK", 475 Body: ioutil.NopCloser(strings.NewReader(`{"items":[{"index": {}}]}`))}, nil 476 }, 477 }}) 478 479 cfg := BulkIndexerConfig{ 480 NumWorkers: 1, 481 Client: client, 482 FlushInterval: 50 * time.Millisecond, // Decrease the flush timeout 483 } 484 if os.Getenv("DEBUG") != "" { 485 cfg.DebugLogger = log.New(os.Stdout, "", 0) 486 } 487 488 bi, _ := NewBulkIndexer(cfg) 489 490 bi.Add(context.Background(), 491 BulkIndexerItem{Action: "index", Body: strings.NewReader(`{"title":"foo"}`)}) 492 493 // Allow some time for auto-flush to kick in 494 time.Sleep(250 * time.Millisecond) 495 496 stats := bi.Stats() 497 expected := uint64(1) 498 499 if stats.NumAdded != expected { 500 t.Errorf("Unexpected NumAdded: want=%d, got=%d", expected, stats.NumAdded) 501 } 502 503 if stats.NumFailed != 0 { 504 t.Errorf("Unexpected NumFailed: want=%d, got=%d", 0, stats.NumFlushed) 505 } 506 507 if stats.NumFlushed != expected { 508 t.Errorf("Unexpected NumFlushed: want=%d, got=%d", expected, stats.NumFlushed) 509 } 510 511 if stats.NumIndexed != expected { 512 t.Errorf("Unexpected NumIndexed: want=%d, got=%d", expected, stats.NumIndexed) 513 } 514 515 // Wait some time before closing the indexer to clear the timer 516 time.Sleep(200 * time.Millisecond) 517 bi.Close(context.Background()) 518 }) 519 520 t.Run("TooManyRequests", func(t *testing.T) { 521 var ( 522 wg sync.WaitGroup 523 524 countReqs int 525 numItems = 2 526 ) 527 528 cfg := opensearch.Config{ 529 Transport: &mockTransport{ 530 RoundTripFunc: func(request *http.Request) (*http.Response, error) { 531 if request.URL.Path == "/" { 532 return &http.Response{ 533 StatusCode: http.StatusOK, 534 Status: "200 OK", 535 Body: ioutil.NopCloser(strings.NewReader(infoBody)), 536 Header: http.Header{"Content-Type": []string{"application/json"}}, 537 }, nil 538 } 539 540 countReqs++ 541 if countReqs <= 4 { 542 return &http.Response{ 543 StatusCode: http.StatusTooManyRequests, 544 Status: "429 TooManyRequests", 545 Body: ioutil.NopCloser(strings.NewReader(`{"took":1}`))}, nil 546 } 547 bodyContent, _ := ioutil.ReadFile("testdata/bulk_response_1c.json") 548 return &http.Response{ 549 StatusCode: http.StatusOK, 550 Status: "200 OK", 551 Body: ioutil.NopCloser(bytes.NewBuffer(bodyContent)), 552 }, nil 553 }, 554 }, 555 556 MaxRetries: 5, 557 RetryOnStatus: []int{502, 503, 504, 429}, 558 RetryBackoff: func(i int) time.Duration { 559 if os.Getenv("DEBUG") != "" { 560 fmt.Printf("*** Retry #%d\n", i) 561 } 562 return time.Duration(i) * 100 * time.Millisecond 563 }, 564 } 565 if os.Getenv("DEBUG") != "" { 566 cfg.Logger = &opensearchtransport.ColorLogger{Output: os.Stdout} 567 } 568 client, _ := opensearch.NewClient(cfg) 569 570 biCfg := BulkIndexerConfig{NumWorkers: 1, FlushBytes: 50, Client: client} 571 if os.Getenv("DEBUG") != "" { 572 biCfg.DebugLogger = log.New(os.Stdout, "", 0) 573 } 574 575 bi, _ := NewBulkIndexer(biCfg) 576 577 for i := 1; i <= numItems; i++ { 578 wg.Add(1) 579 go func(i int) { 580 defer wg.Done() 581 err := bi.Add(context.Background(), BulkIndexerItem{ 582 Action: "foo", 583 Body: strings.NewReader(`{"title":"foo"}`), 584 }) 585 if err != nil { 586 t.Errorf("Unexpected error: %s", err) 587 return 588 } 589 }(i) 590 } 591 wg.Wait() 592 593 if err := bi.Close(context.Background()); err != nil { 594 t.Errorf("Unexpected error: %s", err) 595 } 596 597 stats := bi.Stats() 598 599 if stats.NumAdded != uint64(numItems) { 600 t.Errorf("Unexpected NumAdded: want=%d, got=%d", numItems, stats.NumAdded) 601 } 602 603 if stats.NumFlushed != uint64(numItems) { 604 t.Errorf("Unexpected NumFlushed: want=%d, got=%d", numItems, stats.NumFlushed) 605 } 606 607 if stats.NumFailed != 0 { 608 t.Errorf("Unexpected NumFailed: want=%d, got=%d", 0, stats.NumFailed) 609 } 610 611 // Stats don't include the retries in client 612 if stats.NumRequests != 1 { 613 t.Errorf("Unexpected NumRequests: want=%d, got=%d", 3, stats.NumRequests) 614 } 615 }) 616 617 t.Run("Custom JSON Decoder", func(t *testing.T) { 618 client, _ := opensearch.NewClient(opensearch.Config{Transport: &mockTransport{}}) 619 bi, _ := NewBulkIndexer(BulkIndexerConfig{Client: client, Decoder: customJSONDecoder{}}) 620 621 err := bi.Add(context.Background(), BulkIndexerItem{ 622 Action: "index", 623 DocumentID: "1", 624 Body: strings.NewReader(`{"title":"foo"}`), 625 }) 626 if err != nil { 627 t.Fatalf("Unexpected error: %s", err) 628 } 629 630 if err := bi.Close(context.Background()); err != nil { 631 t.Errorf("Unexpected error: %s", err) 632 } 633 634 stats := bi.Stats() 635 636 if stats.NumAdded != uint64(1) { 637 t.Errorf("Unexpected NumAdded: %d", stats.NumAdded) 638 } 639 }) 640 641 t.Run("Worker.writeMeta()", func(t *testing.T) { 642 type args struct { 643 item BulkIndexerItem 644 } 645 tests := []struct { 646 name string 647 args args 648 want string 649 }{ 650 { 651 "without _index and _id", 652 args{BulkIndexerItem{Action: "index"}}, 653 `{"index":{}}` + "\n", 654 }, 655 { 656 "with _id", 657 args{BulkIndexerItem{ 658 Action: "index", 659 DocumentID: "42", 660 }}, 661 `{"index":{"_id":"42"}}` + "\n", 662 }, 663 { 664 "with _index", 665 args{BulkIndexerItem{ 666 Action: "index", 667 Index: "test", 668 }}, 669 `{"index":{"_index":"test"}}` + "\n", 670 }, 671 { 672 "with _index and _id", 673 args{BulkIndexerItem{ 674 Action: "index", 675 DocumentID: "42", 676 Index: "test", 677 }}, 678 `{"index":{"_index":"test","_id":"42"}}` + "\n", 679 }, 680 { 681 "with if_seq_no and if_primary_term", 682 args{BulkIndexerItem{ 683 Action: "index", 684 DocumentID: "42", 685 Index: "test", 686 IfSeqNum: int64Pointer(5), 687 IfPrimaryTerm: int64Pointer(1), 688 }}, 689 `{"index":{"_index":"test","_id":"42","if_seq_no":5,"if_primary_term":1}}` + "\n", 690 }, 691 { 692 "with version and no document, if_seq_no, and if_primary_term", 693 args{BulkIndexerItem{ 694 Action: "index", 695 Index: "test", 696 Version: int64Pointer(23), 697 }}, 698 `{"index":{"_index":"test"}}` + "\n", 699 }, 700 { 701 "with version", 702 args{BulkIndexerItem{ 703 Action: "index", 704 DocumentID: "42", 705 Index: "test", 706 Version: int64Pointer(24), 707 }}, 708 `{"index":{"_index":"test","_id":"42","version":24}}` + "\n", 709 }, 710 { 711 "with version and version_type", 712 args{BulkIndexerItem{ 713 Action: "index", 714 DocumentID: "42", 715 Index: "test", 716 Version: int64Pointer(25), 717 VersionType: strPointer("external"), 718 }}, 719 `{"index":{"_index":"test","_id":"42","version":25,"version_type":"external"}}` + "\n", 720 }, 721 { 722 "wait_for_active_shards", 723 args{BulkIndexerItem{ 724 Action: "index", 725 DocumentID: "42", 726 Index: "test", 727 Version: int64Pointer(25), 728 VersionType: strPointer("external"), 729 WaitForActiveShards: 1, 730 }}, 731 `{"index":{"_index":"test","_id":"42","version":25,"version_type":"external","wait_for_active_shards":1}}` + "\n", 732 }, 733 { 734 "wait_for_active_shards, all", 735 args{BulkIndexerItem{ 736 Action: "index", 737 DocumentID: "42", 738 Index: "test", 739 Version: int64Pointer(25), 740 VersionType: strPointer("external"), 741 WaitForActiveShards: "all", 742 }}, 743 `{"index":{"_index":"test","_id":"42","version":25,"version_type":"external","wait_for_active_shards":"all"}}` + "\n", 744 }, 745 { 746 "with retry_on_conflict", 747 args{BulkIndexerItem{ 748 Action: "index", 749 DocumentID: "42", 750 Index: "test", 751 Version: int64Pointer(25), 752 VersionType: strPointer("external"), 753 RetryOnConflict: intPointer(5), 754 }}, 755 `{"index":{"_index":"test","_id":"42","version":25,"version_type":"external","retry_on_conflict":5}}` + "\n", 756 }, 757 } 758 for _, tt := range tests { 759 tt := tt 760 761 t.Run(tt.name, func(t *testing.T) { 762 w := &worker{ 763 buf: bytes.NewBuffer(make([]byte, 0, 5e+6)), 764 aux: make([]byte, 0, 512), 765 } 766 if err := w.writeMeta(tt.args.item); err != nil { 767 t.Errorf("Unexpected error: %v", err) 768 } 769 770 if w.buf.String() != tt.want { 771 t.Errorf("worker.writeMeta() %s = got [%s], want [%s]", tt.name, w.buf.String(), tt.want) 772 } 773 774 }) 775 } 776 }) 777 } 778 779 type customJSONDecoder struct{} 780 781 func (d customJSONDecoder) UnmarshalFromReader(r io.Reader, blk *BulkIndexerResponse) error { 782 return json.NewDecoder(r).Decode(blk) 783 } 784 785 func strPointer(s string) *string { 786 return &s 787 } 788 789 func int64Pointer(i int64) *int64 { 790 return &i 791 } 792 793 func intPointer(i int) *int { 794 return &i 795 }