Skip to content

feat(targets): add configurable batch wait time limit - #3757

Draft
imamAtif wants to merge 6 commits into
meltano:mainfrom
imamAtif:feat/1626-batch-wait-limit
Draft

feat(targets): add configurable batch wait time limit#3757
imamAtif wants to merge 6 commits into
meltano:mainfrom
imamAtif:feat/1626-batch-wait-limit

Conversation

@imamAtif

@imamAtif imamAtif commented Aug 30, 2026

Copy link
Copy Markdown

Summary

  • add a new target-level setting batch_wait_limit_seconds that sets the maximum time a batch can remain open before being processed, regardless of row count
  • track batch start time in Sink._batch_start_time and reset it after each drain
  • add Sink.is_too_old property to check elapsed time against the configured limit
  • update Sink.is_full to trigger a drain when the time limit is exceeded
  • drain expired batches at the start of record processing so batches that exceeded their limit while idle are handled before the next record

This addresses the case where a target runs in a memory-constrained environment and cannot afford to wait for large batches to fill up. The existing batch_size_rows setting controls the row count; this adds the complementary time-based control.

Validation

  • uv run pytest -q — 859 passed, 388 deselected, 1 xfailed, 21 subtests passed
  • mypy on changed files — passed

Files changed

  • singer_sdk/helpers/capabilities.py — new TARGET_BATCH_WAIT_LIMIT_SECONDS_CONFIG
  • singer_sdk/sinks/core.pybatch_wait_limit_seconds, _batch_start_time, is_too_old, updated is_full and mark_drained
  • singer_sdk/target_base.py — import and merge new config in append_builtin_config, drain expired sinks at start of record processing
  • tests/core/test_target_base.py — 10 new tests for config, is_too_old, is_full integration, drain reset, about info, negative values, expired batch drain, timer init
  • tests/sql/test_target.py — added batch_wait_limit_seconds to expected default settings

Closes #1626

Summary by Sourcery

Add time-based batch draining for targets to prevent low-volume batches from remaining open indefinitely.

New Features:

  • Add a configurable target-level time limit that processes open batches after they have waited too long, complementing row-count batch sizing.

Bug Fixes:

  • Ensure expired batches are drained before processing a new record, including batches that expired while the target was idle.

Enhancements:

  • Track batch lifetimes and reset the timer after each batch is drained.
  • Expose the batch wait limit through target configuration and generated settings metadata, while ignoring negative values.

Tests:

  • Add coverage for batch wait configuration, expiration behavior, drain integration, timer lifecycle, metadata exposure, and invalid values.

Add a new target-level setting `batch_wait_limit_seconds` that allows
users to set a maximum time a batch can remain open before being
processed, regardless of how many rows it contains.

This addresses the case where a target runs in a memory-constrained
environment and cannot afford to wait for large batches to fill up.

- Add `TARGET_BATCH_WAIT_LIMIT_SECONDS_CONFIG` to built-in config
- Track batch start time in `Sink._batch_start_time`
- Add `Sink.is_too_old` property to check elapsed time
- Update `Sink.is_full` to trigger drain when time limit is exceeded
- Reset batch start time in `Sink.mark_drained()`

Closes meltano#1626
@sourcery-ai

sourcery-ai Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces the optional batch_wait_limit_seconds target setting, tracks each batch’s start time, and makes sinks drain when the configured age limit is reached in addition to the existing row-count threshold, with corresponding configuration and behavior tests.

Sequence diagram for time-based batch draining

sequenceDiagram
    participant Target
    participant Sink
    Target->>Sink: _after_process_record(context)
    alt first record in batch
        Sink->>Sink: _batch_start_time = time.time()
    end
    Target->>Sink: is_full
    Sink->>Sink: current_size >= max_size
    Sink->>Sink: is_too_old
    Sink->>Sink: time.time() - _batch_start_time >= batch_wait_limit_seconds
    alt row limit or wait limit reached
        Target->>Sink: mark_drained()
        Sink->>Sink: _batch_start_time = None
    end
Loading

File-Level Changes

Change Details Files
Adds a target-level batch age limit and exposes it through built-in target configuration.
  • Defines the batch_wait_limit_seconds integer setting and descriptive metadata.
  • Merges the setting into target schemas and validates its presence in about information and default settings.
singer_sdk/helpers/capabilities.py
singer_sdk/target_base.py
tests/core/test_target_base.py
tests/sql/test_target.py
Tracks batch lifetime and uses it as an additional drain condition.
  • Captures the start time when the first record is processed.
  • Adds batch_wait_limit_seconds and is_too_old sink properties, with no-op behavior when unset or before a batch starts.
  • Updates is_full to drain when either row capacity or elapsed time reaches its limit.
  • Clears the batch start time after draining.
singer_sdk/sinks/core.py
Adds coverage for configuration, timeout behavior, drain integration, and metadata exposure.
  • Tests unset, active, and expired wait limits.
  • Tests is_full behavior and timer reset after draining.
  • Tests target about-information and SQL target default settings.
tests/core/test_target_base.py
tests/sql/test_target.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1626 Provide built-in target settings for configuring maximum batch rows and maximum batch wait time, with batch size falling back to the existing default when unset.
#1626 Track the start of each batch and expose logic to determine whether the batch has exceeded the configured wait limit.
#1626 Use the wait-limit check during target processing so batches are drained when either the row limit or time limit is reached, and reset timing after a drain.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@read-the-docs-community

read-the-docs-community Bot commented Aug 30, 2026

Copy link
Copy Markdown

Documentation build overview

📚 Meltano SDK | 🛠️ Build #34298464 | 📁 Comparing 9d592cb against latest (747fc9a)

  🔍 Preview build  

2 files changed
± genindex.html
± classes/singer_sdk.Sink.html

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="singer_sdk/sinks/core.py" line_range="335" />
<code_context>
     def is_full(self) -> bool:
         """True if the sink needs to be drained."""
-        return self.current_size >= self.max_size
+        return self.current_size >= self.max_size or self.is_too_old

     @property
</code_context>
<issue_to_address>
**issue (bug_risk):** The time limit is checked only after a record has been processed, and the batch timer is started only in `_after_process_record`; a batch that has already exceeded its limit is not drained until another record arrives, while time spent processing the first record is excluded from the limit. A low-volume stream therefore keeps an open batch beyond the configured maximum.

**Triggers:** When no record arrives after the batch exceeds its time limit, or when processing the first record itself takes longer than the configured limit.

**Suggested fix:** Start the timer before processing the first record and add a timer-driven or end-of-input drain path so expired batches do not depend on a subsequent record.
</issue_to_address>

### Comment 2
<location path="singer_sdk/helpers/capabilities.py" line_range="308-315" />
<code_context>
     ),
 ).to_dict()
+TARGET_BATCH_WAIT_LIMIT_SECONDS_CONFIG = PropertiesList(
+    Property(
+        "batch_wait_limit_seconds",
+        IntegerType,
+        title="Batch Wait Limit (Seconds)",
+        description=(
+            "Maximum number of seconds to wait for a batch to reach "
+            "the configured batch size before processing it anyway."
+        ),
+    ),
+).to_dict()
</code_context>
<issue_to_address>
**issue (bug_risk):** The new setting accepts arbitrary integers because `IntegerType` has no non-negative constraint, so a negative value passes config validation and makes `is_too_old` true immediately after the first record, forcing every batch to drain after each record.

**Triggers:** When a user configures a negative `batch_wait_limit_seconds` value.

**Suggested fix:** Constrain the schema to non-negative values, or reject values below zero when the sink is initialized.
</issue_to_address>

### Comment 3
<location path="singer_sdk/sinks/core.py" line_range="358" />
<code_context>
+        """True if the current batch has exceeded the wait time limit."""
+        if self._batch_start_time is None or self.batch_wait_limit_seconds is None:
+            return False
+        return (time.time() - self._batch_start_time) >= self.batch_wait_limit_seconds
+
     @property
</code_context>
<issue_to_address>
**issue (bug_risk):** Elapsed time is calculated with `time.time()`, which is wall-clock time; a system clock adjustment backward makes the elapsed duration negative and delays draining beyond the configured limit, while a forward adjustment causes premature draining.

**Triggers:** When the system clock is corrected or synchronized while a batch is open.

**Suggested fix:** Use `time.monotonic()` for batch elapsed-time measurements and initialize/reset the timer from the same monotonic clock.
</issue_to_address>

Sourcery assessment

Approval pending. 3 findings to address first.

Blocking findings: singer_sdk/sinks/core.py:335, singer_sdk/helpers/capabilities.py:315, singer_sdk/sinks/core.py:358


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread singer_sdk/sinks/core.py
Comment thread singer_sdk/helpers/capabilities.py
Comment thread singer_sdk/sinks/core.py Outdated
@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.42%. Comparing base (747fc9a) to head (9d592cb).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3757      +/-   ##
==========================================
+ Coverage   94.35%   94.42%   +0.06%     
==========================================
  Files          74       74              
  Lines        6294     6314      +20     
  Branches      770      775       +5     
==========================================
+ Hits         5939     5962      +23     
+ Misses        266      264       -2     
+ Partials       89       88       -1     
Flag Coverage Δ
core 83.87% <95.45%> (+0.94%) ⬆️
end-to-end 75.59% <72.72%> (-0.02%) ⬇️
optional-components 44.96% <27.27%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed-hq

codspeed-hq Bot commented Aug 30, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 14 untouched benchmarks


Comparing imamAtif:feat/1626-batch-wait-limit (9d592cb) with main (747fc9a)

Open in CodSpeed

imamAtif and others added 5 commits August 30, 2026 16:23
- use time.monotonic() instead of time.time() to avoid issues with
  system clock adjustments
- clamp negative batch_wait_limit_seconds to None (disabled) instead
  of allowing invalid values through
- move batch timer start from _after_process_record to
  preprocess_record so it starts before the first record is processed,
  not after
Check all active sinks for expired batches at the start of
_process_record_message, so batches that exceeded their wait time
limit while idle are drained before processing the next record.
Add tests for expired batch drain flow and preprocess_record timer
initialization to improve codecov patch coverage.
Update the expired batch test to go through _process_record_message
with a properly registered stream, covering the drain loop at the
start of record processing.
@imamAtif

imamAtif commented Aug 30, 2026

Copy link
Copy Markdown
Author

Re: sourcery review comment

All three concerns addressed in subsequent commits:

  1. Timer started after processing : moved _batch_start_time from _after_process_record to preprocess_record (runs before process_record), so processing time is now included in the elapsed check.

  2. Expired batch not drained until next record : added a drain check at the start of _process_record_message that iterates all active sinks and drains any with is_full == True (including time-expired batches) before processing the incoming record.

  3. time.time() clock issues : changed to time.monotonic().

  4. Negative values : clamped to None (disabled) in Sink.__init__.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Configurable batch size and max wait limit for targets

1 participant