-
Notifications
You must be signed in to change notification settings - Fork 161
feat[opensearchutil]: route subsequent bulk requests on the same documentID to the same worker. #950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat[opensearchutil]: route subsequent bulk requests on the same documentID to the same worker. #950
Changes from all commits
b5b48db
8f03ea9
01941fa
581abd3
74fd793
e3f717c
57b0b4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,13 +34,16 @@ import ( | |
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
| "runtime" | ||
| "strings" | ||
| "sync" | ||
| "sync/atomic" | ||
| "time" | ||
|
|
||
| "github.com/opensearch-project/opensearch-go/v5/opensearchapi" | ||
| "github.com/opensearch-project/opensearch-go/v5/opensearchtransport" | ||
| "github.com/opensearch-project/opensearch-go/v5/opensearchutil/shardhash" | ||
| ) | ||
|
|
||
| const defaultFlushInterval = 30 * time.Second | ||
|
|
@@ -166,10 +169,16 @@ type BulkIndexerDebugLogger interface { | |
| } | ||
|
|
||
| type bulkIndexer struct { | ||
| wg sync.WaitGroup | ||
| queue chan BulkIndexerItem | ||
| workers []*worker | ||
| ticker *time.Ticker | ||
| wg sync.WaitGroup | ||
| queues struct { | ||
| sync.RWMutex | ||
| m map[*opensearchtransport.Connection]chan BulkIndexerItem | ||
| } | ||
| rrQueues []chan BulkIndexerItem | ||
| docRouter *opensearchtransport.DocRouter | ||
| rrCounter atomic.Uint64 // added for round-robin fallback | ||
| workers []*worker | ||
| ticker *time.Ticker | ||
|
sean- marked this conversation as resolved.
|
||
| // stopFlush cancels the flusher goroutine; flusherDone is closed when that | ||
| // goroutine returns. Close cancels via stopFlush (non-blocking and | ||
| // idempotent, so Close never deadlocks even when the flusher already | ||
|
|
@@ -235,10 +244,15 @@ func NewBulkIndexer(cfg BulkIndexerConfig) (BulkIndexer, error) { | |
| if cfg.MetaBufferPoolMaxBytes == 0 { | ||
| cfg.MetaBufferPoolMaxBytes = defaultMetaBufferPoolMaxBytes | ||
| } | ||
| docRouter, err := opensearchtransport.NewDocRouter() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| bi := bulkIndexer{ | ||
| config: cfg, | ||
| stats: &bulkIndexerStats{}, | ||
| docRouter: docRouter, | ||
| metaPoolMaxBytes: cfg.MetaBufferPoolMaxBytes, | ||
| implicitClient: implicitClient, | ||
| metaPool: sync.Pool{ | ||
|
|
@@ -254,18 +268,69 @@ func NewBulkIndexer(cfg BulkIndexerConfig) (BulkIndexer, error) { | |
| return &bi, nil | ||
| } | ||
|
|
||
| // Add adds an item to the indexer. | ||
| // Add adds an item to the indexer and routes it to the correct worker queue. | ||
| // | ||
| // Adding an item after a call to Close() will panic. | ||
| func (bi *bulkIndexer) Add(ctx context.Context, item BulkIndexerItem) error { | ||
| var targetQueue chan BulkIndexerItem | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, we'd take a |
||
|
|
||
| //nolint:nestif // keep routing logic inline for simplicity | ||
| if item.DocumentID != "" { | ||
| idx := item.Index | ||
| if idx == "" { | ||
| idx = bi.config.Index | ||
| } | ||
|
|
||
| encodedPath := "/" + url.PathEscape(idx) + "/_doc/" + url.PathEscape(item.DocumentID) | ||
|
|
||
| if p, err := url.PathUnescape(encodedPath); err == nil { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question about the escape/unescape pair here: does the escaping survive to Eval? |
||
| req := (&http.Request{ | ||
| Method: http.MethodPost, | ||
| URL: &url.URL{ | ||
| Path: p, | ||
| RawPath: encodedPath, | ||
| }, | ||
| }).WithContext(ctx) | ||
|
|
||
| if hop, evalErr := bi.docRouter.Eval(ctx, req); evalErr == nil && hop.Conn != nil { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this Eval ever return a connection? I think NewDocRouter() gets its connections through DiscoveryUpdate(), which only the owning transport calls, so activeConns looks like it stays empty and Eval returns early at policy_doc_router.go:144-150. |
||
| bi.queues.RLock() | ||
| targetQueue = bi.queues.m[hop.Conn] | ||
| bi.queues.RUnlock() | ||
|
|
||
| if targetQueue == nil { | ||
| bi.queues.Lock() | ||
| targetQueue = bi.queues.m[hop.Conn] | ||
| if targetQueue == nil { | ||
| //nolint:gosec // G115: NumWorkers is strictly positive | ||
| workerIndex := bi.rrCounter.Add(1) % uint64(bi.config.NumWorkers) | ||
| targetQueue = bi.rrQueues[workerIndex] | ||
| bi.queues.m[hop.Conn] = targetQueue | ||
| } | ||
| bi.queues.Unlock() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if targetQueue == nil { | ||
| //nolint:gosec // G115: intentional conversion from signed to unsigned for modulo | ||
| workerIndex := uint32(shardhash.Hash(item.DocumentID)) % uint32(bi.config.NumWorkers) | ||
| targetQueue = bi.rrQueues[workerIndex] | ||
| } | ||
| } else { | ||
| // Round-robin distribution for items without a DocumentID | ||
| //nolint:gosec // G115: NumWorkers is strictly positive | ||
| workerIndex := bi.rrCounter.Add(1) % uint64(bi.config.NumWorkers) | ||
| targetQueue = bi.rrQueues[workerIndex] | ||
| } | ||
|
|
||
| select { | ||
| case <-ctx.Done(): | ||
| bi.stats.bulkAddFailCount.Add(1) | ||
| if bi.config.OnError != nil { | ||
| bi.config.OnError(ctx, ctx.Err()) | ||
| } | ||
| return ctx.Err() | ||
| case bi.queue <- item: | ||
| case targetQueue <- item: | ||
| bi.stats.numAdded.Add(1) | ||
| } | ||
|
|
||
|
|
@@ -276,7 +341,12 @@ func (bi *bulkIndexer) Add(ctx context.Context, item BulkIndexerItem) error { | |
| // stops the flusher goroutine and calls flush on all writers. | ||
| func (bi *bulkIndexer) Close(ctx context.Context) error { | ||
| bi.ticker.Stop() | ||
| close(bi.queue) | ||
|
|
||
| // Iterate through the slice and close each worker's channel | ||
| for _, q := range bi.rrQueues { | ||
| close(q) | ||
| } | ||
|
|
||
| // Stop the periodic flusher and wait for it to return before the final | ||
| // drain below, so no auto-flush races the drain. stopFlush is non-blocking | ||
| // and idempotent; flusherDone is already closed if the flusher exited via | ||
|
|
@@ -339,18 +409,22 @@ func (bi *bulkIndexer) Stats() BulkIndexerStats { | |
|
|
||
| // init initializes the bulk indexer. | ||
| func (bi *bulkIndexer) init(ctx context.Context) { | ||
| bi.queue = make(chan BulkIndexerItem, bi.config.NumWorkers) | ||
| bi.queues.m = make(map[*opensearchtransport.Connection]chan BulkIndexerItem) | ||
| bi.rrQueues = make([]chan BulkIndexerItem, bi.config.NumWorkers) | ||
|
|
||
| for i := 1; i <= bi.config.NumWorkers; i++ { | ||
| ch := make(chan BulkIndexerItem, bi.config.NumWorkers) | ||
| bi.rrQueues[i-1] = ch | ||
| w := worker{ | ||
| id: i, | ||
| ch: bi.queue, | ||
| ch: ch, | ||
| bi: bi, | ||
| buf: bytes.NewBuffer(make([]byte, 0, bi.config.FlushBytes)), | ||
| } | ||
| w.run(ctx) | ||
| bi.workers = append(bi.workers, &w) | ||
| } | ||
|
|
||
| bi.wg.Add(bi.config.NumWorkers) | ||
|
|
||
| bi.ticker = time.NewTicker(bi.config.FlushInterval) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See https://github.com/opensearch-project/opensearch-go/blob/main/guides/transport-routing.md#document-path-docrouter and https://github.com/opensearch-project/opensearch-go/blob/main/guides/transport-routing.md#shared-connections-across-policies