Skip to content

feat: Batch flush - #3975

Merged
juliusgeo merged 201 commits into
mainfrom
feat--batch-flush-redux
Jul 23, 2026
Merged

feat: Batch flush#3975
juliusgeo merged 201 commits into
mainfrom
feat--batch-flush-redux

Conversation

@juliusgeo

@juliusgeo juliusgeo commented May 20, 2026

Copy link
Copy Markdown
Contributor

Description

#2617
Adds concept of "dynamic batches" that allow the workflow to specify either a max batch size, or a batch timeout time, or both, that will flush the batch to the worker once either of those limits are reached. Batch scheduling is done by moving rows from v1_queue_item to v1_batch_queue_item where they will be picked up by the batch scheduler. The dispatcher will send a "START_BATCH" action to workers, containing all the payloads in the batch. If the payloads in a batch are about to exceed the 4mb gRPC limit, then the batch will automatically be flushed, resulting in lower batch sizes.

Assorted architectural notes:

  • BatchSchedulers are guarded by tenant leases, scoped per step. Batch keys are handled inside the BatchScheduler
  • Messages sending the batches to and from the engine are always a singular message, (so START / FAILURE / SUCCESS / CANCELLED states) to make sure that batches are always executed atomically. A failure in the batch will propagate back to all of the call sites.
  • If broadcast_output is set to true, then the output from the batch step function will be returned to all call sites as-is. If it is false, then the output (a dictionary mapping task ids to outputs), will send the outputs under each key to the respective call site it originated from.
  • Batch group keys can be used to partition different task runs in the same workflow into separate batches, with the same batch settings. So if you want to make sure all of Tenant A's tasks get executed as part of one function, and same for Tenant B, you set a batch key like input.tenant_id.
  • Batches will be flushed when one of the following conditions is true:
    1. The batches max size is reached
    2. The batches max interval is reached
    3. The payloads of the tasks queued for a batch are about to breach the 4mb GRPC limit.

Fixes # (issue)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Documentation change (pure documentation change)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking changes to code which doesn't change any behaviour)
  • CI (any automation pipeline changes)
  • Chore (changes which are not directly related to any business logic)
  • Test changes (add, refactor, improve or change a test)
  • This change requires a documentation update

What's Changed

  • Engine/scheduler/dispatcher support
  • Go SDK support
  • Typescript SDK support
  • Python SDK support
  • Ruby SDK support

Checklist

Changes have been:

  • Tested (unit, integration, or manually with steps specified)
  • Linted and formatted
  • Documented (where applicable)
  • Added to CHANGELOG (where applicable) -- see Keep a Changelog

🤖 AI Disclosure
  • I acknowledge that an LLM was used in the creation of this Pull Request, in accordance with Hatchet's AI_POLICY.md.
  • Details: [e.g. generating tests, writing docs]

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Optional test failure: The load-deadlock job failed on this PR (optimistic-scheduling=false). This check is non-mandatory and does not block merging, but may be worth investigating. View logs

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Optional test failure: The load-deadlock job failed on this PR (optimistic-scheduling=true). This check is non-mandatory and does not block merging, but may be worth investigating. View logs

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Optional test failure: The load-deadlock job failed on this PR (optimistic-scheduling=true). This check is non-mandatory and does not block merging, but may be worth investigating. View logs

Comment thread sql/schema/v1-core.sql
WHERE tr.tenant_id = br.tenant_id
AND tr.batch_id = br.batch_id
)
FOR UPDATE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always good practice for every FOR UPDATE to have an ORDER BY, or you risk deadlocking

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!

LIMIT
COALESCE(sqlc.narg('limit')::integer, 1000);

-- name: ListDistinctBatchResources :many

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these leases are going to be acquired for every single step_id, even those which are inactive, correct? Like if a workflow has 100 versions, each with batching configured, we'll acquire 100 leases? this is problematic, we need to limit it to "active" steps somehow

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, it doesn't acquire for steps that are inactive because it joins with v1_batched_queue_item which is just the in-progress batch items. So unless those 100 versions all had items in flight, there wouldn't be 100 leases.

-- +goose Up
-- +goose StatementBegin
-- v0 schema alignment
ALTER TYPE "LeaseKind" ADD VALUE 'BATCH';

@mrkaye97 mrkaye97 Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know I'm usually the "don't use if not exists" guy, but we need it here 😅 since there's no way to remove a value from an enum, we have to have it otherwise we can't run this migration twice (same for the others below)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oof, fixed

@abelanger5 abelanger5 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some non-blocking comments, but looks good to me!

}
}

runtimes, err := d.repov1.Tasks().ListTaskRuntimes(ctx, tenantId, bulkDatas)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we've added so many things to this method (ListTasks, durable invocations, parent outputs, payloads, and now this) - should we try to write a new repository method so that we can reduce database round-trips?


// Confirm the worker belongs to the auth-tenant before mutating its labels.
if _, err := s.repov1.Workers().GetWorkerForEngine(ctx, tenant.ID, workerId); err != nil {
if _, err := s.repov1.Workers().GetWorkerForEngine(ctx, tenant.ID, workerId); err != nil { //nolint

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does lint fail? it's probably just a matter of renaming err or something

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not seeing spans/traces in this file (or really in that many places in this PR) - should definitely do a quick pass and make sure we're instrumenting the highest-risk calls.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Optional test failure: The load-online-migrate job failed on this PR. This check is non-mandatory and does not block merging, but may be worth investigating. View logs

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Optional test failure: The load-online-migrate job failed on this PR. This check is non-mandatory and does not block merging, but may be worth investigating. View logs

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Optional test failure: The load-online-migrate job failed on this PR. This check is non-mandatory and does not block merging, but may be worth investigating. View logs

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Optional test failure: The load-online-migrate job failed on this PR. This check is non-mandatory and does not block merging, but may be worth investigating. View logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dashboard Related to the Hatchet dashboard documentation Improvements or additions to documentation engine Related to the core Hatchet engine sdk-go Related to the Go SDK sdk-py Related to the Python sdk sdk-ruby Related to the Ruby SDK sdk-ts Related to the Typescript SDK

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants