Skip to content

UN-2801 [FIX] Fix file batch distribution algorithm to respect configured batch limits#1530

Merged
jaseemjaskp merged 1 commit into
mainfrom
fix/UN-2801-FIX_improve_file_batch_distribution_for_parallel_processing
Sep 14, 2025
Merged

UN-2801 [FIX] Fix file batch distribution algorithm to respect configured batch limits#1530
jaseemjaskp merged 1 commit into
mainfrom
fix/UN-2801-FIX_improve_file_batch_distribution_for_parallel_processing

Conversation

@muhammad-ali-e

Copy link
Copy Markdown
Contributor

What

  • Fixed file batch distribution algorithm in WorkflowHelper.get_file_batches() method
  • Resolved issue where system was creating fewer batches than configured (e.g., 16 batches instead of 30)

Why

  • The previous implementation using math.ceil(num_files / num_batches) was causing uneven distribution
  • Files were not being distributed optimally across parallel workers
  • System was not respecting the configured MAX_PARALLEL_FILE_BATCHES setting properly

How

  • Replaced ceiling-based calculation with integer division and remainder distribution
  • Files are now distributed as evenly as possible across the target number of batches
  • First 'remainder' batches get one extra file to ensure even distribution
  • Added comprehensive docstring explaining the distribution logic

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No, this cannot break existing features
  • The change only affects how files are distributed into batches for parallel processing
  • The total number of files processed remains the same
  • All files are still processed, just distributed more evenly

Database Migrations

  • None required

Env Config

  • No changes to environment configuration

Relevant Docs

  • N/A

Related Issues or PRs

  • Jira Ticket: UN-2801

Dependencies Versions

  • No dependency changes

Notes on Testing

  • Test with various file counts to ensure even distribution
  • Verify that configured batch limits are respected
  • Check logs to confirm correct number of batches are created

Screenshots

N/A

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

…ured batch limits

Fixed an issue where the file batching algorithm was creating fewer batches than configured (16 instead of 30), causing uneven distribution of files across parallel workers. The new implementation ensures files are distributed as evenly as possible across the target number of batches.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 14, 2025

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Bug Fixes
    • Prevents creation of empty batches and safely handles zero-file inputs, reducing failed or stalled workflows.
  • Refactor
    • Reworked batching to evenly distribute files across batches, improving load balance, throughput consistency, and fairness across workers for more predictable parallel execution.
  • Documentation
    • Updated internal documentation to clarify batching behavior and expected outcomes.

Walkthrough

Replaced fixed-size batching with an even-distribution algorithm in get_file_batches, handling zero files, ensuring non-empty batches, and updating the docstring. Removed an unused math import. No public signatures changed.

Changes

Cohort / File(s) Summary of Changes
Batching logic update
backend/workflow_manager/workflow_v2/workflow_helper.py
Rewrote batching to evenly distribute files across min(BATCH_SIZE, num_files) batches using base/remainder calculation; ensured no empty batches; added zero-input handling; updated docstring; removed math import.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the primary change—fixing the file batch distribution algorithm—and includes the Jira ticket for traceability; it directly reflects the update to WorkflowHelper.get_file_batches and the intended outcome of respecting configured batch limits. The wording is concise, specific, and relevant to the changeset, making it clear to reviewers and future readers. It avoids extraneous file lists or emojis and focuses on the main fix.
Description Check ✅ Passed The PR description closely follows the repository template and includes the required sections (What, Why, How, breakage assessment, Database Migrations, Env Config, Related Issues, Dependencies, Notes on Testing, Screenshots, and Checklist), so it is largely complete and actionable for reviewers. Non-critical fields like Relevant Docs and Screenshots are marked N/A which is acceptable for this non-UI change. The description provides clear context, justification, and testing guidance for verifying the new batching behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/UN-2801-FIX_improve_file_batch_distribution_for_parallel_processing

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/workflow_manager/workflow_v2/workflow_helper.py (1)

88-99: Return type annotation is incorrect (JSON dict vs FileHash).

You convert FileHash to JSON before batching, so the function returns list[list[tuple[str, dict]]], not FileHash. Fix the annotation and clarify in the docstring.

-    def get_file_batches(
-        cls, input_files: dict[str, FileHash]
-    ) -> list[list[tuple[str, FileHash]]]:
-        """Split input files into batches for parallel processing.
-        Distributes files as evenly as possible across the target number of batches.
+    def get_file_batches(
+        cls, input_files: dict[str, FileHash]
+    ) -> list[list[tuple[str, dict[str, Any]]]]:
+        """Split input files into batches for parallel processing.
+        Distributes files as evenly as possible across the target number of batches.
+        Returns batches of (file_name, serialized FileHash as dict).
🧹 Nitpick comments (1)
backend/workflow_manager/workflow_v2/workflow_helper.py (1)

128-133: Remove redundant guards to simplify loop.

start_index < len(file_items) and the non-empty check are redundant once batch_size is computed as above and num_files > 0. Simplify for readability.

-            if start_index < len(file_items):
-                end_index = min(start_index + batch_size, len(file_items))
-                batch = file_items[start_index:end_index]
-                if batch:  # Only add non-empty batches
-                    batches.append(batch)
-                start_index = end_index
+            end_index = start_index + batch_size
+            batches.append(file_items[start_index:end_index])
+            start_index = end_index
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 3276baf and 84e6db1.

📒 Files selected for processing (1)
  • backend/workflow_manager/workflow_v2/workflow_helper.py (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
backend/workflow_manager/workflow_v2/workflow_helper.py (2)

124-127: Even distribution logic looks correct.

Remainder-first allocation is the right approach to keep size deltas ≤ 1. Nice.


91-93: Summary/docstring mismatch: zero-files case not actually handled here.

AI summary mentions explicit zero-input handling returning [], but the method would currently divide by zero without the guard suggested above.

Comment on lines 106 to +119
BATCH_SIZE = Configuration.get_value_by_organization(
config_key=ConfigKey.MAX_PARALLEL_FILE_BATCHES, organization=organization
) # Max number of batches

file_items = list(json_serializable_files.items())

# Calculate how many items per batch
# Calculate distribution
num_files = len(file_items)
# Target number of batches (can't exceed number of files)
num_batches = min(BATCH_SIZE, num_files)
items_per_batch = math.ceil(num_files / num_batches)

# Split into batches
# Distribute files as evenly as possible
base_items_per_batch = num_files // num_batches
remainder = num_files % num_batches

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.

💡 Verification agent

🧩 Analysis chain

Prevent ZeroDivisionError; cast and clamp MAX_PARALLEL_FILE_BATCHES; handle empty input.

If input_files is empty or the config resolves to 0/None/non‑int, num_batches becomes 0 and base_items_per_batch = num_files // num_batches will raise ZeroDivisionError. Also, Configuration.get_value_by_organization may return strings; min(str, int) will TypeError. Guard and normalize.

Apply:

-        BATCH_SIZE = Configuration.get_value_by_organization(
-            config_key=ConfigKey.MAX_PARALLEL_FILE_BATCHES, organization=organization
-        )  # Max number of batches
+        raw_batches = Configuration.get_value_by_organization(
+            config_key=ConfigKey.MAX_PARALLEL_FILE_BATCHES, organization=organization
+        )  # Max number of batches (may be str/None)
+        try:
+            BATCH_SIZE = max(1, int(raw_batches))
+        except (TypeError, ValueError):
+            BATCH_SIZE = 1
         file_items = list(json_serializable_files.items())
 
-        # Calculate distribution
-        num_files = len(file_items)
+        # Calculate distribution
+        num_files = len(file_items)
+        if num_files == 0:
+            return []
 
         # Target number of batches (can't exceed number of files)
         num_batches = min(BATCH_SIZE, num_files)

Quick checks:


🏁 Script executed:

#!/bin/bash
# 1) Find any other callers that might pass empty input
rg -n -C2 '\bget_file_batches\s*\('

# 2) Sanity-test the distribution property on representative values (no repo import)
python - <<'PY'
def sizes(n, b):
    from math import ceil
    if n == 0: return []
    b = max(1, int(b))
    nb = min(b, n)
    base, r = divmod(n, nb)
    return [base + (1 if i < r else 0) for i in range(nb)]
for n in [0,1,2,3,10,31,60]:
    for b in [0,1,2,3,30,64,"30","not-int",None]:
        try:
            s = sizes(n,b)
            ok = sum(s)==n and (max(s, default=0)-min(s, default=0) <= 1)
            print(f"n={n}, b={b!r} -> {s} | ok={ok}")
        except Exception as e:
            print(f"n={n}, b={b!r} -> EXC {e}")
PY

Length of output: 1041


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== get_file_batches definition (first 300 lines) ==="
nl -ba backend/workflow_manager/workflow_v2/workflow_helper.py | sed -n '1,300p'
echo
echo "=== callers of get_file_batches ==="
rg -n -C2 '\bget_file_batches\s*\(' || true
echo
echo "=== occurrences of MAX_PARALLEL_FILE_BATCHES ==="
rg -n 'MAX_PARALLEL_FILE_BATCHES' -S || true
echo
echo "=== sanity python test ==="
python - <<'PY'
def sizes(n,b):
    if n==0:
        return []
    try:
        b_int = max(1, int(b))
    except Exception:
        b_int = 1
    nb = min(b_int, n)
    base, r = divmod(n, nb)
    return [base + (1 if i < r else 0) for i in range(nb)]
tests_n = [0,1,2,3,10,31,60]
tests_b = [0,1,2,3,30,64,"30","not-int",None]
for n in tests_n:
    for b in tests_b:
        try:
            s = sizes(n,b)
            ok = sum(s)==n and (max(s, default=0)-min(s, default=0) <= 1)
            print(f"n={n}, b={b!r} -> {s} | ok={ok}")
        except Exception as e:
            print(f"n={n}, b={b!r} -> EXC {e}")
PY

Length of output: 94


🏁 Script executed:

#!/bin/bash
# Print relevant portions of the file and search for callers / config keys
echo "=== backend/workflow_manager/workflow_v2/workflow_helper.py (lines 1-300) ==="
sed -n '1,300p' backend/workflow_manager/workflow_v2/workflow_helper.py || true
echo
echo "=== callers of get_file_batches ==="
rg -n -C2 '\bget_file_batches\s*\(' || true
echo
echo "=== occurrences of MAX_PARALLEL_FILE_BATCHES ==="
rg -n 'MAX_PARALLEL_FILE_BATCHES' -S || true
echo
echo "=== quick sanity python test for distribution logic ==="
python - <<'PY'
def sizes(n,b):
    if n==0:
        return []
    try:
        b_int = max(1, int(b))
    except Exception:
        b_int = 1
    nb = min(b_int, n)
    base, r = divmod(n, nb)
    return [base + (1 if i < r else 0) for i in range(nb)]

tests_n = [0,1,2,3,10,31,60]
tests_b = [0,1,2,3,30,64,"30","not-int",None]
for n in tests_n:
    for b in tests_b:
        try:
            s = sizes(n,b)
            ok = sum(s)==n and (max(s, default=0)-min(s, default=0) <= 1)
            print(f"n={n}, b={b!r} -> {s} | ok={ok}")
        except Exception as e:
            print(f"n={n}, b={b!r} -> EXC {e}")
PY

Length of output: 13726


Cast & clamp MAX_PARALLEL_FILE_BATCHES; prevent TypeError/ZeroDivisionError

  • process_input_files already returns early when input_files is empty, so num_files == 0 is not reachable from current callers.
  • Mandatory: normalize the configuration value before use — replace the direct read with a safe cast/fallback to ensure an int >= 1 (avoid TypeError in min() and possible division by zero). Example change:
raw_batches = Configuration.get_value_by_organization(
    config_key=ConfigKey.MAX_PARALLEL_FILE_BATCHES, organization=organization
)
try:
    BATCH_SIZE = max(1, int(raw_batches))
except (TypeError, ValueError):
    BATCH_SIZE = 1
  • Optional hardening: keep if num_files == 0: return [] inside get_file_batches for defense-in-depth.

Location: backend/workflow_manager/workflow_v2/workflow_helper.py (around lines 106–119).

🤖 Prompt for AI Agents
In backend/workflow_manager/workflow_v2/workflow_helper.py around lines 106 to
119, the code reads ConfigKey.MAX_PARALLEL_FILE_BATCHES directly which can be
None or non-int and lead to TypeError/ZeroDivisionError; replace the direct read
with a safe normalization: fetch the raw config, attempt to cast to int inside a
try/except, clamp to at least 1 (e.g., BATCH_SIZE = max(1, int(raw))) and
fallback to 1 on TypeError/ValueError, then proceed using that BATCH_SIZE;
optionally also add a defensive early return if num_files == 0 to guard against
future callers.

@jaseemjaskp jaseemjaskp merged commit 64fa6c7 into main Sep 14, 2025
7 checks passed
@jaseemjaskp jaseemjaskp deleted the fix/UN-2801-FIX_improve_file_batch_distribution_for_parallel_processing branch September 14, 2025 11:39
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.

3 participants