Skip to content

[FEATURE] Add an exported flush function to the BulkIndexer #336

Description

@ae-ou

Is your feature request related to a problem?

I have a Lambda that consumes events from a Kinesis stream and posts them to an OpenSearch Cluster - I use BulkIndexer to send in a large number of documents more efficiently (i.e. via the /bulk API).

Currently, I build up most of my dependencies (e.g. DB connection pools, AWS Signer, etc) in the main() function of my Lambda, and then I pass these dependencies to a struct. I create a handler function which receives the struct containing all of these dependencies - this way, as long as my Lambda stays warm, the dependencies are readily available for the handler to use on a subsequent invocation. e.g.:

package main

import (
	"context"
	"database/sql"
	"github.com/aws/aws-lambda-go/lambda"
)

//Dependencies can be reused across subsequent invocations of the Lambda - which speeds up non-cold starts (as we don't have to create the dependencies for injection)
type Dependencies struct {
	DbCon *sql.DB
	//... more dependencies
}

func main() {
	myDb := sql.OpenDB() //an imaginary DB that you still shouldn't call DROP on.

	d := Dependencies{
		DbCon: myDb,
	}

	lambda.Start(d.MyHandler)
}

func (d Dependencies) MyHandler(c context.Context) error {
	tx, err := d.DbCon.Begin()
	//Business logic here
}

I want to be able to pass the BulkIndexer on my dependency struct, but there's an issue. Your documentation for the BulkIndexer says:

// You must call the Close() method after you're done adding items.

The problem is that when you call Close() against BulkIndexer, you close queue (which is the channel that Add() appends requests to).
This creates a problem on subsequent invocations of the Lambda because if the channel is closed (by the call to Close() on a previous invocation), then calling Add() will result in trying to pass data to a nonexistent channel - which results in a panic.

This means that I can't store the BulkIndexer in my dependency struct because the channel underpinning the whole thing may be nonexistent on subsequent invocations. This means that I have to instantiate the BulkIndexer for each invocation of the Lambda (directly on the handler - even if the function is still warm).

What solution would you like?

I would like to be able to call Flush() directly against the BulkIndexer.

The Close() function already calls flush() against the workers, and this fires off whatever requests may be remaining in the queue (in addition to invoking the relevant callback functions).
If we were able to call BulkIndexer.Flush() directly, we could clear out whatever requests may remain in the queue (without closing it), and then call return on the Lambda afterwards - this would allow us to reuse the BulkIndexer on subsequent invocations of the Lambda (without risk of closing the channel/causing a panic).

What alternatives have you considered?

  1. Setting BulkIndexerConfig.FlushInterval.
    a. The problem here is that we have to wait for the timer to hit 0 for the flush to occur - this means that we have to keep the Lambda running (which costs money and inflates our execution time metrics).
    b. There's also no clean way to see the amount of time until the next flush.
    c. You could set BulkIndexerConfig.FlushInterval to a low number - but if you're flushing/firing off requests every few seconds, you defeat the purpose of using the /bulk API.
  2. Setting BulkIndexerConfig.FlushBytes
    a. The problem here is that we have to wait for the BulkIndexer to hit the threshold - which isn't always guaranteed to happen - e.g. if you've just flushed the data, but then you Add() one small request.
    b. You could set BulkIndexerConfig.FlushBytes to a low number - but if you're flushing/firing off requests after a small number of Add() calls, you defeat the purpose of using the /bulk API.
  3. Create the BulkIndexer in the handler - every time that the Lambda is called
    a. This is what I do currently
    b. This means that you can call Close() at the end of your handler function (to flush any outstanding requests in the queue)
    c. This significantly increases the runtime of my Lambda (on every invocation), which has a direct impact on running cost.

Do you have any additional context?

Setting up dependencies outside of a Lambda's function handler is an AWS best practice - see here:

Take advantage of execution environment reuse to improve the performance of your function. Initialize SDK clients and database connections outside of the function handler, and cache static assets locally in the /tmp directory. Subsequent invocations processed by the same instance of your function can reuse these resources. This saves cost by reducing function run time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions